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

from typing import Optional


@Gtk.Template(resource_path="/org/gnome/gitlab/cheywood/Iotas/ui/editor_search_header_bar.ui")
class EditorSearchHeaderBar(Adw.Bin):
    __gtype_name__ = "EditorSearchHeaderBar"
    __gsignals__ = {
        "resumed": (GObject.SignalFlags.RUN_FIRST, None, ()),
    }

    _entry = Gtk.Template.Child()
    _backward_button = Gtk.Template.Child()
    _forward_button = Gtk.Template.Child()

    def __init__(self):
        super().__init__()

        self.__settings = GtkSource.SearchSettings.new()
        self.__settings.set_wrap_around(True)
        self.__settings.bind_property(
            "search-text",
            self._entry.text,
            "text",
            GObject.BindingFlags.BIDIRECTIONAL,
        )
        self.__context = None
        self.__offset_when_entered = 0
        self.__jump_back_on_hide = False
        self.__context_signal_handler_id = None
        self.__cursor_signal_handler_id = None
        self.__current_match_tag = None

    def setup(self, sourceview: GtkSource.View):
        """Perform initial setup."""
        self.__setup_actions()
        self.__sourceview = sourceview

    def enter(self, resuming: bool) -> None:
        """Enter search.

        :param bool resuming: Whether resuming search
        """
        buffer = self.__sourceview.get_buffer()
        self.__context = GtkSource.SearchContext.new(buffer, self.__settings)
        insert_iter = buffer.get_iter_at_mark(buffer.get_insert())
        self.__offset_when_entered = insert_iter.get_offset()
        self.__setup_current_match_styling()
        if not resuming:
            self._entry.select_all_and_focus()

        self.__context_signal_handler_id = self.__context.connect(
            "notify::occurrences-count", self.__occurrences_count_changed
        )
        self.__cursor_signal_handler_id = buffer.connect("cursor-moved", self.__on_cursor_moved)

    def exit(self) -> None:
        """Exit search."""
        buffer = self.__sourceview.get_buffer()
        buffer.disconnect(self.__cursor_signal_handler_id)
        self.__context.disconnect(self.__context_signal_handler_id)
        self.__context = None
        begin, end = buffer.get_bounds()
        buffer.remove_tag(self.__current_match_tag, begin, end)
        self.__current_match_tag = None
        self.__jump_back_on_hide = False

    @GObject.Property(type=bool, default=False)
    def jump_back_on_hide(self) -> bool:
        return self.__jump_back_on_hide

    @jump_back_on_hide.setter
    def jump_back_on_hide(self, value: bool) -> None:
        self.__jump_back_on_hide = value

    def __on_backward(self, _obj: GObject.Object, result: Gio.AsyncResult) -> None:
        success, match_start, match_end, __ = self.__context.backward_finish(result)
        if success:
            buffer = self.__sourceview.get_buffer()
            buffer.select_range(match_start, match_end)
            self.__current_match_tag.set_priority(buffer.get_tag_table().get_size() - 1)
            self.__sourceview.scroll_to_insertion_point()
            self.__jump_back_on_hide = False
            self.__update_for_current_match(match_start, match_end)

    def __on_forward(self, _obj: GObject.Object, result: Gio.AsyncResult) -> None:
        success, match_start, match_end, __ = self.__context.forward_finish(result)
        if success:
            buffer = self.__sourceview.get_buffer()
            buffer.select_range(match_start, match_end)
            self.__current_match_tag.set_priority(buffer.get_tag_table().get_size() - 1)
            self.__sourceview.scroll_to_insertion_point()
            self.__jump_back_on_hide = False
            self.__update_for_current_match(match_start, match_end)

    def __on_cursor_moved(self, _obj: GObject.Object) -> None:
        self.__update_for_current_match(None, None)

    def __setup_actions(self) -> None:
        action_group = Gio.SimpleActionGroup.new()
        app = Gio.Application.get_default()

        action = Gio.SimpleAction.new("forward")
        action.connect("activate", self.__move_forward)
        action_group.add_action(action)
        app.set_accels_for_action("editor-search.forward", ["<Control>g"])

        action = Gio.SimpleAction.new("backward")
        action.connect("activate", self.__move_backward)
        action_group.add_action(action)
        app.set_accels_for_action("editor-search.backward", ["<Shift><Control>g"])

        self.__action_group = action_group
        app.get_active_window().insert_action_group("editor-search", action_group)

    def __jump_to_first(self) -> None:
        buffer = self.__sourceview.get_buffer()
        start_iter = buffer.get_iter_at_offset(self.__offset_when_entered)
        success, match_start, __, __ = self.__context.forward(start_iter)
        if success:
            self.__sourceview.scroll_to_iter(match_start, 0.25, True, 1, 0.5)
            self.__jump_back_on_hide = True

    def __occurrences_count_changed(
        self,
        _obj: GObject.Object,
        _value: GObject.ParamSpec,
    ) -> None:
        count = self.__context.get_occurrences_count()
        self._entry.set_occurrence_count(count)
        search_can_move = count > 0
        if search_can_move:
            self.__jump_to_first()
        self.__action_group.lookup_action("backward").set_enabled(search_can_move)
        self.__action_group.lookup_action("forward").set_enabled(search_can_move)

    def __move_backward(self, _action: Gio.SimpleAction, _param: GLib.Variant) -> None:
        """Move to previous search match."""
        if self.__context is None:
            if self._entry.get_text() != "":
                self.emit("resumed")
            else:
                return
        buffer = self.__sourceview.get_buffer()
        if buffer.get_has_selection():
            begin, end = buffer.get_selection_bounds()
            begin.order(end)
        else:
            mark = buffer.get_insert()
            begin = buffer.get_iter_at_mark(mark)
        self.__context.backward_async(begin, None, self.__on_backward)
        self.__jump_back_on_hide = False

    def __move_forward(
        self,
        _action: Gio.SimpleAction,
        _param: GLib.Variant,
    ) -> None:
        """Move to next search match."""
        if self.__context is None:
            if self._entry.get_text() != "":
                self.emit("resumed")
            else:
                return
        buffer = self.__sourceview.get_buffer()
        if buffer.get_has_selection():
            begin, end = buffer.get_selection_bounds()
            begin.order(end)
        else:
            mark = buffer.get_insert()
            end = buffer.get_iter_at_mark(mark)
        self.__context.forward_async(end, None, self.__on_forward)
        self.__jump_back_on_hide = False

    def __update_for_current_match(
        self, match_start: Optional[Gtk.TextIter], match_end: Optional[Gtk.TextIter]
    ) -> None:
        buffer = self.__sourceview.get_buffer()

        new_position = 0
        if buffer.get_has_selection():
            bounds = buffer.get_selection_bounds()
            if len(bounds) == 2:
                begin, end = bounds
                begin.order(end)
                new_position = self.__context.get_occurrence_position(begin, end)
        self._entry.set_occurrence_position(new_position)

        begin, end = buffer.get_bounds()
        buffer.remove_tag(self.__current_match_tag, begin, end)
        if match_start is not None and match_end is not None:
            buffer.apply_tag(self.__current_match_tag, match_start, match_end)
            self.__current_match_tag.set_priority(buffer.get_tag_table().get_size() - 1)

    def __setup_current_match_styling(self) -> None:
        buffer = self.__sourceview.get_buffer()
        table = buffer.get_tag_table()
        scheme = buffer.get_style_scheme()

        if self.__current_match_tag is not None:
            table.remove(self.__current_match_tag)
        self.__current_match_tag = buffer.create_tag()

        if scheme is not None:
            style = scheme.get_style("current-search-match")
            if style is not None:
                style.apply(self.__current_match_tag)
        self.__current_match_tag.set_priority(table.get_size() - 1)
