# This file is part of Gajim.
#
# SPDX-License-Identifier: GPL-3.0-only

from __future__ import annotations

import logging
import sys
import uuid

from gi.repository import Gdk
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import Gtk
from nbxmpp.errors import StanzaError
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.task import Task

from gajim.common import app
from gajim.common import configpaths
from gajim.common import events
from gajim.common import ged
from gajim.common.commands import ChatCommands
from gajim.common.const import CallType
from gajim.common.const import Display
from gajim.common.ged import EventHelper
from gajim.common.helpers import idle_add_once
from gajim.common.i18n import _
from gajim.common.modules.contacts import BareContact
from gajim.common.modules.contacts import GroupchatContact
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.storage.archive.const import ChatDirection
from gajim.common.storage.archive.const import MessageType
from gajim.common.storage.archive.models import Message
from gajim.common.structs import OutgoingMessage
from gajim.common.types import ChatContactT
from gajim.common.util.muc import message_needs_highlight
from gajim.common.util.preview import get_preview_data
from gajim.common.util.text import remove_invalid_xml_chars

from gajim.gtk.activity_page import ActivityPage
from gajim.gtk.alert import InformationAlertDialog
from gajim.gtk.chat_banner import ChatBanner
from gajim.gtk.chat_function_page import ChatFunctionPage
from gajim.gtk.chat_function_page import FunctionMode
from gajim.gtk.control import ChatControl
from gajim.gtk.message_actions_box import MessageActionsBox
from gajim.gtk.message_input import MessageInputTextView
from gajim.gtk.util.classes import SignalManager
from gajim.gtk.util.misc import allow_send_message
from gajim.gtk.util.misc import gi_gui_package_version
from gajim.gtk.util.misc import transform_to_inverted_bool
from gajim.gtk.util.window import open_window

log = logging.getLogger("gajim.gtk.chatstack")


class ChatStack(Gtk.Stack, EventHelper, SignalManager):
    __gtype_name__ = "ChatStack"

    def __init__(self):
        Gtk.Stack.__init__(self, hexpand=True, vexpand=True)
        EventHelper.__init__(self)
        SignalManager.__init__(self)

        self.set_hhomogeneous(False)
        self.set_vhomogeneous(False)

        self._current_contact: ChatContactT | None = None
        self._last_quoted_id: int | None = None

        self.add_named(ChatPlaceholderBox(), "empty")

        self._activity_page = ActivityPage()
        self.add_named(self._activity_page, "activity")

        self._chat_function_page = ChatFunctionPage()
        self._chat_function_page.connect("finish", self._on_function_finished)
        self._chat_function_page.connect("message", self._on_function_message)
        self.add_named(self._chat_function_page, "function")

        self._chat_banner = ChatBanner()
        self._chat_control = ChatControl()
        self._message_action_box = MessageActionsBox()

        app.commands.connect("command-error", self._on_command_signal)
        app.commands.connect("command-not-found", self._on_command_signal)
        app.commands.connect("command-result", self._on_command_signal)

        self._banner_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self._banner_box.append(self._chat_banner)
        self._banner_box.append(Gtk.Separator(margin_start=12, margin_end=12))

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.append(self._banner_box)
        box.append(self._chat_control.get_widget())
        box.append(self._message_action_box)

        dnd_icon = Gtk.Image.new_from_icon_name("lucide-paperclip-symbolic")
        dnd_icon.set_vexpand(True)
        dnd_icon.set_valign(Gtk.Align.END)
        dnd_icon.set_pixel_size(64)

        dnd_label = Gtk.Label(label=_("Drop files here"))
        dnd_label.set_max_width_chars(40)
        dnd_label.set_vexpand(True)
        dnd_label.set_valign(Gtk.Align.START)
        dnd_label.add_css_class("title-3")

        self._drop_area = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=18)
        self._drop_area.set_visible(False)
        self._drop_area.set_hexpand(True)
        self._drop_area.set_vexpand(True)
        self._drop_area.add_css_class("background")
        self._drop_area.append(dnd_icon)
        self._drop_area.append(dnd_label)

        overlay = Gtk.Overlay()
        overlay.add_overlay(self._drop_area)
        overlay.set_child(box)

        # TODO: support dnd for contacts (MUC invitations)

        format_builder = Gdk.ContentFormatsBuilder()
        format_builder.add_gtype(Gdk.Texture)
        format_builder.add_gtype(Gdk.FileList)

        drop_target = Gtk.DropTarget(
            actions=Gdk.DragAction.COPY, formats=format_builder.to_formats()
        )
        self._connect(drop_target, "accept", self._on_drop_accept)
        self._connect(drop_target, "drop", self._on_file_drop)
        self._connect(drop_target, "enter", self._on_drag_enter)
        self._connect(drop_target, "leave", self._on_drag_leave)
        overlay.add_controller(drop_target)

        controller = Gtk.EventControllerKey()
        self._connect(controller, "key-pressed", self._on_key_pressed)
        self.add_controller(controller)

        self.add_named(overlay, "controls")

        self.register_events(
            [
                ("message-received", 85, self._on_message_received),
                ("muc-disco-update", 85, self._on_muc_disco_update),
                ("account-connected", 85, self._on_account_state),
                ("account-disconnected", 85, self._on_account_state),
                ("register-actions", ged.GUI1, self._on_register_actions),
                ("feature-discovered", ged.GUI1, self._on_feature_discovered),
            ]
        )

    def do_unroot(self) -> None:
        Gtk.Stack.do_unroot(self)
        self._disconnect_all()
        self.unregister_events()
        app.check_finalize(self)

    def _on_register_actions(self, _event: events.RegisterActions) -> None:
        actions = [
            "send-file",
            "send-file-httpupload",
            # 'send-file-jingle',
            "start-video-call",
            "start-voice-call",
            "send-message",
            "show-contact-info",
            "show-chat-share",
            "muc-change-nickname",
            "muc-invite",
            "muc-contact-info",
            "muc-execute-command",
            "muc-ban",
            "muc-kick",
            "muc-change-role",
            "muc-change-affiliation",
            "muc-request-voice",
            "quote-next",
            "quote-prev",
        ]

        for action in actions:
            action = app.window.lookup_action(action)
            assert action is not None
            action.connect("activate", self._on_action)

        focus_action = app.window.lookup_action("focus-mode")
        assert focus_action is not None
        focus_action.bind_property(
            "state",
            self._banner_box,
            "visible",
            transform_to=transform_to_inverted_bool,
        )

    def _get_current_contact(self) -> ChatContactT:
        assert self._current_contact is not None
        return self._current_contact

    def process_escape(self) -> bool:
        if self.get_visible_child_name() == "function":
            self._chat_function_page.process_escape()
            return True

        if self._chat_control.process_escape():
            return True

        return self._message_action_box.process_escape()

    def get_chat_control(self) -> ChatControl:
        return self._chat_control

    def set_narrow(self, narrow: bool) -> None:
        self._chat_banner.set_narrow(narrow)
        self._chat_control.set_narrow(narrow)

    def get_message_action_box(self) -> MessageActionsBox:
        return self._message_action_box

    def get_message_input(self) -> MessageInputTextView:
        return self._message_action_box.get_message_input()

    def show_chat(self, account: str, jid: JID) -> None:
        # Store (preserve) primary clipboard and restore it after switching
        clipboard = self.get_primary_clipboard()
        if clipboard.get_content() is not None:
            clipboard.read_text_async(
                None, self._on_primary_clipboard_read_text_finished
            )

        self._last_quoted_id = None

        if self._current_contact is not None:
            self._current_contact.disconnect_all_from_obj(self)

        client = app.get_client(account)
        contact = client.get_module("Contacts").get_contact(jid)
        assert isinstance(
            contact, BareContact | GroupchatContact | GroupchatParticipant
        )
        self._current_contact = contact

        self._chat_banner.switch_contact(self._current_contact)
        self._chat_control.switch_contact(self._current_contact)
        self._message_action_box.switch_contact(self._current_contact)

        self._update_base_actions(self._current_contact)

        if isinstance(self._current_contact, GroupchatContact):
            self._current_contact.multi_connect(
                {
                    "user-joined": self._on_user_joined,
                    "user-role-changed": self._on_user_role_changed,
                    "user-affiliation-changed": self._on_user_affiliation_changed,
                    "state-changed": self._on_muc_state_changed,
                    "room-password-required": self._on_room_password_required,
                    "room-captcha-challenge": self._on_room_captcha_challenge,
                    "room-captcha-error": self._on_room_captcha_error,
                    "room-creation-failed": self._on_room_creation_failed,
                    "room-join-failed": self._on_room_join_failed,
                    "room-config-failed": self._on_room_config_failed,
                }
            )
            self._update_group_chat_actions(self._current_contact)

        elif isinstance(self._current_contact, GroupchatParticipant):
            self._update_participant_actions(self._current_contact)

        else:
            self._update_chat_actions(self._current_contact)

        if isinstance(self._current_contact, GroupchatContact):
            muc_data = client.get_module("MUC").get_muc_data(self._current_contact.jid)
            if muc_data is not None:
                if muc_data.state.is_captcha_request:
                    self._show_chat_function_page(FunctionMode.CAPTCHA_REQUEST)
                    return

                if muc_data.state.is_password_request:
                    self._show_chat_function_page(FunctionMode.PASSWORD_REQUEST)
                    return

                if not muc_data.state.is_joined:
                    if muc_data.error == "captcha-failed":
                        self._show_chat_function_page(
                            FunctionMode.CAPTCHA_ERROR, muc_data.error_text
                        )
                        return
                    if muc_data.error == "join-failed":
                        self._show_chat_function_page(
                            FunctionMode.JOIN_FAILED, muc_data.error_text
                        )
                        return
                    if muc_data.error == "creation-failed":
                        self._show_chat_function_page(
                            FunctionMode.CREATION_FAILED, muc_data.error_text
                        )
                        return

        self.set_transition_type(Gtk.StackTransitionType.NONE)
        self.set_visible_child_name("controls")

        app.plugin_manager.extension_point("switch_contact", self._current_contact)

        idle_add_once(app.window.activate_action, "win.input-focus")

    def _on_primary_clipboard_read_text_finished(
        self,
        clipboard: Gdk.Clipboard,
        result: Gio.AsyncResult,
    ) -> None:
        try:
            text = clipboard.read_text_finish(result)
        except Exception as e:
            InformationAlertDialog(_("Pasting Content Failed"), _("Error: %s") % e)
            return

        if text is None:
            log.info("No text pasted")
            return

        # Reset primary clipboard to what it was before switching chats,
        # otherwise it gets overridden.
        clipboard.set(text)

    def show_activity_page(self, context_id: str | None = None) -> None:
        if not context_id:
            self._activity_page.show_default_page()
        self.set_visible_child_name("activity")

    def _on_room_password_required(
        self, _contact: GroupchatContact, _signal_name: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.PASSWORD_REQUEST)

    def _on_room_captcha_challenge(
        self, contact: GroupchatContact, _signal_name: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.CAPTCHA_REQUEST)

    def _on_room_captcha_error(
        self, _contact: GroupchatContact, _signal_name: str, error: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.CAPTCHA_ERROR, error)

    def _on_room_creation_failed(
        self, _contact: GroupchatContact, _signal_name: str, error: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.CREATION_FAILED, error)

    def _on_room_join_failed(
        self, _contact: GroupchatContact, _signal_name: str, error: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.JOIN_FAILED, error)

    def _on_room_config_failed(
        self, _contact: GroupchatContact, _signal_name: str, error: str
    ) -> None:
        self._show_chat_function_page(FunctionMode.CONFIG_FAILED)

    def _on_muc_state_changed(
        self, contact: GroupchatContact, _signal_name: str
    ) -> None:
        self._update_group_chat_actions(contact)

    def _on_user_joined(
        self,
        contact: GroupchatContact,
        _signal_name: str,
        _user_contact: GroupchatParticipant,
        _event: events.MUCUserJoined,
    ) -> None:
        self._update_group_chat_actions(contact)

    def _on_user_role_changed(
        self,
        contact: GroupchatContact,
        _signal_name: str,
        _user_contact: GroupchatParticipant,
        _event: events.MUCUserRoleChanged,
    ) -> None:
        self._update_group_chat_actions(contact)

    def _on_user_affiliation_changed(
        self,
        contact: GroupchatContact,
        _signal_name: str,
        _user_contact: GroupchatParticipant,
        _event: events.MUCUserAffiliationChanged,
    ) -> None:
        self._update_group_chat_actions(contact)

    def _on_muc_disco_update(self, event: events.MucDiscoUpdate) -> None:
        if not isinstance(self._current_contact, GroupchatContact):
            return

        if event.jid != self._current_contact.jid:
            return

        self._update_group_chat_actions(self._current_contact)

    def _on_feature_discovered(self, event: events.FeatureDiscovered) -> None:
        if self._current_contact is None:
            return

        if event.account != self._current_contact.account:
            return

        if event.feature == Namespace.HTTPUPLOAD_0:
            self._update_base_actions(self._current_contact)

    def _on_account_state(
        self, event: events.AccountConnected | events.AccountDisconnected
    ) -> None:
        if self._current_contact is None:
            return

        if event.account != self._current_contact.account:
            return

        self._update_base_actions(self._current_contact)

        if isinstance(self._current_contact, GroupchatContact):
            self._update_group_chat_actions(self._current_contact)
        elif isinstance(self._current_contact, GroupchatParticipant):
            self._update_participant_actions(self._current_contact)
        else:
            self._update_chat_actions(self._current_contact)

    def _on_message_received(self, event: events.MessageReceived) -> None:
        if event.from_mam:
            return

        if (
            app.window.is_chat_active(event.account, event.jid)
            and self._chat_control.view_is_at_bottom()
        ):
            if event.message.id is None:
                return

            client = app.get_client(event.account)
            stanza_id = event.message.stanza_id
            contact = client.get_module("Contacts").get_contact(event.jid)
            assert isinstance(
                contact, BareContact | GroupchatContact | GroupchatParticipant
            )
            mds_assist_sent = client.get_module("ChatMarkers").send_displayed_marker(
                contact, event.message.id, stanza_id
            )

            if not mds_assist_sent and stanza_id is not None:
                by = contact.jid if isinstance(contact, GroupchatContact) else None
                client.get_module("MDS").set_mds(contact.jid, stanza_id, by)

            return

        message = event.message
        if message.direction == ChatDirection.OUTGOING:
            return

        if event.m_type == MessageType.GROUPCHAT:
            client = app.get_client(event.account)
            contact = client.get_module("Contacts").get_contact(
                event.jid, groupchat=True
            )
            assert isinstance(contact, GroupchatContact)
            # MUC messages may be received after some delay, so make sure we
            # don't issue notifications for our own messages.
            self_contact = contact.get_self()
            if self_contact is not None and self_contact.name == message.resource:
                return

        self._issue_notification(event.account, message)

    def _issue_notification(self, account: str, message: Message) -> None:
        text = message.text
        assert text is not None

        client = app.get_client(account)
        contact = client.get_module("Contacts").get_contact(message.remote.jid)

        title = _("New message from")

        preview = get_preview_data(text, message.oob)
        if preview is not None:
            text = preview.text

        sound: str | None = None
        msg_type = "chat-message"
        if isinstance(contact, BareContact):
            msg_type = "chat-message"
            title += f" {contact.name}"
            sound = "first_message_received"
            app.window.set_urgency_hint(True)

        if isinstance(contact, GroupchatContact):
            msg_type = "group-chat-message"
            title += f" {message.resource} ({contact.name})"
            assert contact.nickname is not None
            needs_highlight = message_needs_highlight(
                text, contact.nickname, client.get_own_jid().bare
            )
            if needs_highlight:
                sound = "muc_message_highlight"
            else:
                sound = "muc_message_received"

            if not contact.can_notify() and not needs_highlight:
                return

            app.window.set_urgency_hint(True)

        if isinstance(contact, GroupchatParticipant):
            msg_type = "private-chat-message"
            title += f" {contact.name} (private in {contact.room.name})"
            sound = "first_message_received"

        assert isinstance(
            contact, BareContact | GroupchatContact | GroupchatParticipant
        )
        if app.settings.get("notification_preview_message"):
            if text.startswith("/me "):
                name = contact.name
                if isinstance(contact, GroupchatContact):
                    name = message.resource
                text = f"* {name} {text[3:]}"
        else:
            text = ""

        if isinstance(contact, GroupchatContact):
            resource = message.resource
        else:
            resource = None

        app.ged.raise_event(
            events.Notification(
                context_id="",
                account=contact.account,
                jid=contact.jid,
                type="incoming-message",
                sub_type=msg_type,
                title=title,
                text=text,
                sound=sound,
                resource=resource,
            )
        )

    def _update_base_actions(self, contact: ChatContactT) -> None:
        client = app.get_client(contact.account)
        online = app.account_is_connected(contact.account)

        app.window.get_action("send-message").set_enabled(
            allow_send_message(self.get_message_input().has_text, contact)
        )

        httpupload = app.window.get_action("send-file-httpupload")
        httpupload.set_enabled(online and client.get_module("HTTPUpload").available)

        # jingle = app.window.get_action('send-file-jingle')
        # jingle.set_enabled(online and contact.is_jingle_available)

        app.window.get_action("send-file").set_enabled(
            # jingle.get_enabled() or
            httpupload.get_enabled()
        )

        app.window.get_action("correct-message").set_enabled(online)
        app.window.get_action("retract-message").set_enabled(online)

    def _update_chat_actions(self, contact: BareContact) -> None:
        online = app.account_is_connected(contact.account)

        app.window.get_action("start-voice-call").set_enabled(
            online and contact.supports_audio and sys.platform != "win32"
        )
        app.window.get_action("start-video-call").set_enabled(
            online and contact.supports_video and sys.platform != "win32"
        )

    def _update_group_chat_actions(self, contact: GroupchatContact) -> None:
        joined = contact.is_joined
        is_visitor = False
        if joined:
            self_contact = contact.get_self()
            assert self_contact
            is_visitor = self_contact.role.is_visitor

        app.window.get_action("muc-change-nickname").set_enabled(joined)
        app.window.get_action("muc-contact-info").set_enabled(joined)
        app.window.get_action("muc-execute-command").set_enabled(joined)
        app.window.get_action("muc-ban").set_enabled(joined)
        app.window.get_action("muc-kick").set_enabled(joined)
        app.window.get_action("muc-change-role").set_enabled(joined)
        app.window.get_action("muc-change-affiliation").set_enabled(joined)
        app.window.get_action("muc-invite").set_enabled(joined)

        app.window.get_action("muc-request-voice").set_enabled(is_visitor)

        app.window.get_action("moderate-message").set_enabled(joined)
        app.window.get_action("moderate-all-messages").set_enabled(joined)

    def _update_participant_actions(self, contact: GroupchatParticipant) -> None:
        pass

    def _on_action(self, action: Gio.SimpleAction, param: GLib.Variant | None) -> None:
        if self.get_visible_child_name() != "controls":
            return

        action_name = action.get_name()
        contact = self._current_contact
        if contact is None:
            return

        account = contact.account
        client = app.get_client(account)
        jid = contact.jid

        if action_name == "send-message":
            self._on_send_message()

        elif action_name == "start-voice-call":
            app.call_manager.start_call(account, jid, CallType.AUDIO)

        elif action_name == "start-video-call":
            app.call_manager.start_call(account, jid, CallType.VIDEO)

        elif action_name.startswith("send-file"):
            method = None
            name = action.get_name()
            if "httpupload" in name:
                method = "httpupload"

            # if 'jingle' in name:
            #     method = 'jingle'

            uris = None
            if param is not None:
                uris = param.get_strv()
                if all("" == uri for uri in param.get_strv()):
                    # All list entries are "" (default when invoking "send-file" action)
                    uris = None
            self._show_chat_function_page(FunctionMode.SEND_FILE, method, uris)

        elif action_name == "show-contact-info":
            if isinstance(contact, GroupchatContact):
                open_window("GroupchatDetails", contact=contact)
            else:
                open_window("ContactInfo", account=contact.account, contact=contact)

        elif action_name == "show-chat-share":
            self._chat_banner.present_share_popover()

        elif action_name == "muc-contact-info":
            assert param is not None
            nick = param.get_string()
            assert isinstance(contact, GroupchatContact)
            resource_contact = contact.get_resource(nick)
            open_window("ContactInfo", account=account, contact=resource_contact)

        elif action_name == "muc-invite":
            self._show_chat_function_page(FunctionMode.INVITE)

        elif action_name == "muc-change-nickname":
            self._show_chat_function_page(FunctionMode.CHANGE_NICKNAME)

        elif action_name == "muc-execute-command":
            nick = None
            if param is not None:
                nick = param.get_string()
            if nick:
                assert isinstance(contact, GroupchatContact)
                resource_contact = contact.get_resource(nick)
                jid = resource_contact.jid
            open_window("AdHocCommands", account=account, jids=[jid])

        elif action_name == "muc-kick":
            assert param is not None
            kick_nick = param.get_string()
            self._show_chat_function_page(FunctionMode.KICK, data=kick_nick)

        elif action_name == "muc-ban":
            assert param is not None
            ban_jid = param.get_string()
            self._show_chat_function_page(FunctionMode.BAN, data=ban_jid)

        elif action_name == "muc-change-role":
            assert param is not None
            assert isinstance(contact, GroupchatContact)
            nick, role = param.get_strv()
            client.get_module("MUC").set_role(
                contact.jid,
                nick,
                role,
                callback=self._on_affiliation_or_role_change,
                user_data=(contact, jid, role),
            )

        elif action_name == "muc-change-affiliation":
            assert param is not None
            assert isinstance(contact, GroupchatContact)
            jid, affiliation = param.get_strv()
            client.get_module("MUC").set_affiliation(
                contact.jid,
                {jid: {"affiliation": affiliation}},
                callback=self._on_affiliation_or_role_change,
                user_data=(contact, jid, affiliation),
            )

        elif action_name == "muc-request-voice":
            client.get_module("MUC").request_voice(contact.jid)

        elif action_name.startswith("quote-"):
            view = self._chat_control.get_conversation_view()
            if action_name == "quote-prev":
                row = view.get_prev_message_row(self._last_quoted_id)
            else:
                row = view.get_next_message_row(self._last_quoted_id)

            if row is not None:
                self._last_quoted_id = row.pk
                self._message_action_box.insert_as_quote(row.get_text(), clear=True)

    def _on_affiliation_or_role_change(self, task: Task) -> None:
        muc, jid, affiliation_or_role = task.get_user_data()

        try:
            result = task.finish()
        except StanzaError as error:
            log.error("Error on affiliation/role change request: %s", error)
            if self._current_contact == muc:
                self._chat_control.add_info_message(
                    _(
                        "An error occurred while trying to make "
                        "{occupant_jid} {affiliation_or_role}: {error}"
                    ).format(
                        occupant_jid=jid,
                        affiliation_or_role=affiliation_or_role,
                        error=str(error),
                    )
                )
            else:
                error_message = (
                    _(
                        "An error occurred while trying to make "
                        "{occupant_jid} {affiliation_or_role} in group "
                        '"{group}"'
                    ).format(
                        occupant_jid=jid,
                        affiliation_or_role=affiliation_or_role,
                        group=muc.name,
                    ),
                )
                InformationAlertDialog(_("Error"), f"{error_message}\n{error}")
        else:
            log.debug("Affiliation/role change success: %s", result)

    def _on_drag_enter(
        self,
        _drop_target: Gtk.DropTarget,
        _x: float,
        _y: float,
    ) -> Gdk.DragAction:
        self._drop_area.set_visible(True)
        return Gdk.DragAction.COPY

    def _on_drag_leave(self, _drop_target: Gtk.DropTarget) -> None:
        self._drop_area.set_visible(False)

    def _on_drop_accept(self, target: Gtk.DropTarget, drop: Gdk.Drop) -> bool:
        # Accept handler needs to be here, the default accept handler returns
        # always False, because the overlay is not visible
        allowed_formats = target.get_formats()
        assert allowed_formats is not None

        offered_formats = drop.get_formats()

        log.debug("Offered formats for drop: %s", offered_formats.get_gtypes())
        if not allowed_formats.match(offered_formats):
            return False

        # DND on X11 freezes due to a GTK bug:
        # https://dev.gajim.org/gajim/gajim/-/issues/12313
        if app.is_display(Display.X11):
            return gi_gui_package_version("Gtk>=4.20.1")

        return True

    def _on_file_drop(
        self,
        target: Gtk.DropTargetAsync,
        value: Gdk.FileList | Gdk.Texture | None,
        _x: float,
        _y: float,
    ) -> bool:
        if value is None:
            log.debug("Drop received, but value is None")
            return False

        if not self._chat_control.has_active_chat():
            return False

        files: list[str] = []

        log.debug("Drop received: %s", value)
        if isinstance(value, Gdk.Texture):
            try:
                image = value.save_to_png_bytes()
                image_bytes = image.get_data()
            except GLib.Error as e:
                log.error("Error while trying to store dropped image: %s", e)
                return False

            if image_bytes is None:
                log.error("Could not process dropped image")
                return False

            temp_dir = configpaths.get_temp_dir()
            image_path = temp_dir / f"{uuid.uuid4()}.png"
            try:
                image_path.write_bytes(image_bytes)
            except Exception as e:
                log.error("Could not write dropped image: %s", e)
                return False

            files = [image_path.as_uri()]

        if isinstance(value, Gdk.FileList):
            files = [file.get_uri() for file in value.get_files()]

        if not files:
            log.error("No path for dropped image")
            return False

        self._chat_control.drag_data_file_transfer(files)
        self._drop_area.set_visible(False)
        return True

    def _on_key_pressed(
        self,
        _event_controller_key: Gtk.EventControllerKey,
        keyval: int,
        _keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if not self._chat_control.message_selection_active():
            return Gdk.EVENT_PROPAGATE

        if state & Gdk.ModifierType.CONTROL_MASK:
            if keyval == Gdk.KEY_c:
                self._chat_control.copy_selected_messages()
                return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def _show_chat_function_page(
        self,
        function_mode: FunctionMode,
        data: str | None = None,
        files: list[str] | None = None,
    ) -> None:
        assert self._current_contact is not None
        self._chat_function_page.set_mode(
            self._current_contact, function_mode, data, files
        )
        self.set_transition_type(Gtk.StackTransitionType.SLIDE_UP_DOWN)
        self.set_visible_child_name("function")

    def _on_function_finished(
        self, function_page: ChatFunctionPage, close_control: bool
    ) -> None:
        if close_control:
            self._close_control()
            self.set_visible_child_name("empty")
            self.set_transition_type(Gtk.StackTransitionType.NONE)
            function_page.reset()
            return

        self.set_visible_child_name("controls")
        self.set_transition_type(Gtk.StackTransitionType.NONE)
        function_page.reset()

    def _on_function_message(
        self, _function_page: ChatFunctionPage, message: str
    ) -> None:
        self._chat_control.add_info_message(message)

    def check_send_preconditons(self) -> bool:
        contact = self._current_contact
        assert contact is not None

        client = app.get_client(contact.account)

        encryption = contact.settings.get("encryption")
        if not encryption:
            return True

        if encryption == "OMEMO":
            return client.get_module("OMEMO").check_send_preconditions(contact)

        if encryption == "OpenPGP":
            if not client.get_module("OpenPGP").secret_key_exists():
                open_window("OpenPGPWizard", account=contact.account)
                return False

            return client.get_module("OpenPGP").check_send_preconditions(contact)

        if encryption not in app.plugin_manager.encryption_plugins:
            InformationAlertDialog(
                _("Encryption Error"), _("Missing necessary encryption plugin")
            )
            return False

        self._chat_control.sendmessage = True
        app.plugin_manager.extension_point(
            "send_message" + encryption, self._chat_control
        )
        return self._chat_control.sendmessage

    def _on_send_message(self) -> None:
        message = self._message_action_box.get_text()
        if message.startswith("//"):
            # Escape sequence for chat commands
            message = message[1:]

        contact = self._current_contact
        assert contact is not None

        client = app.get_client(contact.account)

        if not self.check_send_preconditons():
            return

        message = remove_invalid_xml_chars(message)
        if message in ("", "\n"):
            return

        label = self._message_action_box.get_seclabel()
        correct_id = self._message_action_box.get_correction_id()
        reply_data = self._message_action_box.get_message_reply()
        open_graph_data = self._message_action_box.get_open_graph_data()

        chatstate = client.get_module("Chatstate").get_active_chatstate(contact)

        message_ = OutgoingMessage(
            account=contact.account,
            contact=contact,
            text=message,
            chatstate=chatstate,
            sec_label=label,
            control=self._chat_control,
            correct_id=correct_id,
            reply_data=reply_data,
            open_graph_data=open_graph_data,
        )

        client.send_message(message_)

        self._message_action_box.reset_state_after_send()

        self._last_quoted_id = None

    def get_last_message_id(self, contact: ChatContactT) -> str | None:
        return self._message_action_box.get_last_message_id(contact)

    def _close_control(self) -> None:
        assert self._current_contact is not None
        app.window.activate_action(
            "win.remove-chat",
            GLib.Variant(
                "as", [self._current_contact.account, str(self._current_contact.jid)]
            ),
        )

    def clear(self) -> None:
        if self._current_contact is not None:
            self._current_contact.disconnect_all_from_obj(self)

        self._last_quoted_id = None
        self.set_visible_child_name("empty")
        self._chat_banner.clear()
        self._message_action_box.clear()
        self._chat_control.clear()

    def _on_command_signal(
        self, _chat_commands: ChatCommands, signal_name: str, text: str
    ) -> None:
        is_error = signal_name != "command-result"
        self._chat_control.add_command_output(text, is_error)


class ChatPlaceholderBox(Gtk.Box):
    def __init__(self):
        Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL, spacing=18)
        self.set_valign(Gtk.Align.CENTER)
        image = Gtk.Image.new_from_icon_name("gajim-symbolic")
        image.set_pixel_size(100)
        image.set_opacity(0.2)
        self.append(image)

        button = Gtk.Button(
            label=_("Start Chatting…"),
            halign=Gtk.Align.CENTER,
            action_name="app.start-chat",
            action_target=GLib.Variant("as", ["", ""]),
        )
        self.append(button)
