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

from __future__ import annotations

from typing import Any

import logging
from collections.abc import Callable

from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk

from gajim.common import app
from gajim.common import events
from gajim.common import ged
from gajim.common.i18n import _
from gajim.common.util.text import format_duration

from gajim.gtk.builder import get_builder
from gajim.gtk.preview.audio import AudioPreviewWidget
from gajim.gtk.preview.audio_visualizer import AudioVisualizerWidget
from gajim.gtk.util.classes import SignalManager
from gajim.gtk.voice_message_recorder import GST_ERROR_ON_RECORDING
from gajim.gtk.voice_message_recorder import GST_ERROR_ON_START
from gajim.gtk.voice_message_recorder import VoiceMessageRecorder

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

TIME_LABEL_UPDATE_DELAY = 200
BAR_INTERVAL_MS = 62  # ≈ 5000 ms / 80 bars


class VoiceMessageRecorderButton(Gtk.MenuButton, SignalManager):
    def __init__(self) -> None:
        Gtk.MenuButton.__init__(self, visible=False, valign=Gtk.Align.CENTER)
        SignalManager.__init__(self)

        self._time_label_update_timeout_id = None
        self._bar_timeout_id = None
        self._frame_tick_id: int | None = None
        self._last_bar_ts: int = 0

        self._mic_button_long_pressed = False
        self._early_recording_started = False

        self._new_recording = True

        self.set_visible(app.settings.get("show_voice_message_button"))
        app.settings.bind_signal("show_voice_message_button", self, "set_visible")

        self._ui = get_builder("voice_message_recorder.ui")

        try:
            self._voice_message_recorder = VoiceMessageRecorder(self._on_error_occurred)
        except Exception:
            log.exception("Error while initializing VoiceMessageRecorder")

        if not hasattr(self, "_voice_message_recorder"):
            self.set_sensitive(False)
            self.set_tooltip_text(_("Voice Messages are not available"))
            self.set_child(Gtk.Image.new_from_icon_name("lucide-mic-symbolic"))
            return

        self._connect(
            self._voice_message_recorder,
            "notify::available",
            self._on_recorder_available,
        )

        gesture_direct_record_pressed = Gtk.GestureLongPress(button=Gdk.BUTTON_PRIMARY)
        gesture_direct_record_pressed.set_propagation_phase(
            Gtk.PropagationPhase.CAPTURE
        )
        self._connect(
            gesture_direct_record_pressed, "begin", self._on_direct_record_begin
        )
        self._connect(
            gesture_direct_record_pressed, "pressed", self._on_direct_record_pressed
        )
        self._connect(
            gesture_direct_record_pressed, "cancelled", self._on_direct_record_cancelled
        )
        self.add_controller(gesture_direct_record_pressed)

        gesture_direct_record_released = Gtk.GestureLongPress(button=Gdk.BUTTON_PRIMARY)
        self._connect(
            gesture_direct_record_released, "end", self._on_direct_record_long_press_end
        )
        self.add_controller(gesture_direct_record_released)

        # TODO: When creating the widget, the file path is initialized with some value,
        # which will be overwritten with the first recording.
        # This is fine in this case, b/c we generate a static ID for the audio player.
        # We might want to handle this a little bit more intuitive though in the future.
        assert app.audio_player is not None
        self._audio_player_widget = AudioPreviewWidget(
            app.audio_player, "", 0, self._voice_message_recorder.audio_file_abspath
        )

        self._connect(self._ui.cancel_button, "clicked", self._on_cancel_clicked)
        self._connect(
            self._ui.record_toggle_button, "clicked", self._on_record_toggle_clicked
        )
        self._connect(self._ui.send_button, "clicked", self._on_send_clicked)

        self.set_popover(self._ui.popover)
        self._connect(self._ui.popover, "closed", self._on_popover_closed)

        self._audio_visualizer = AudioVisualizerWidget()
        self._audio_visualizer.set_parameters(1.0, live_mode=True)
        self._audio_visualizer.set_visible(True)

        self._ui.visualization_box.append(self._audio_visualizer)
        self._ui.progression_box.set_visible(True)

        self._ui.audio_player_box.append(self._audio_player_widget)
        self._ui.audio_player_box.set_visible(False)

        self._update_button_state()
        self._update_icons()
        self._audio_visualizer.set_samples(
            self._voice_message_recorder.recording_samples()
        )
        self._audio_visualizer.render_animated_graph(0.0)
        app.ged.register_event_handler(
            "register-actions", ged.GUI1, self._on_register_actions
        )

    def run_destroy(self) -> None:
        self._disconnect_all()
        self._audio_player_widget.run_destroy()
        app.settings.disconnect_signals(self)
        app.check_finalize(self)

    def _on_register_actions(self, _event: events.RegisterActions) -> None:
        action = app.window.get_action("send-file-httpupload")
        self._connect(action, "notify::enabled", self._on_send_file_action_changed)

    def _on_send_file_action_changed(self, *args: Any) -> None:
        self._update_button_state()
        self._update_icons()

    def _on_recorder_available(self, *args: Any) -> None:
        self._update_button_state()
        self._update_icons()

    def _update_button_state(self) -> None:
        if self._voice_messages_available():
            self.set_sensitive(True)
            tooltip_text = _("Record Voice Message… (hold button to record directly)")
        else:
            self.set_sensitive(False)
            tooltip_text = _("Voice Messages are not available")

        self.set_tooltip_text(tooltip_text)

    def _update_icons(self) -> None:
        if self._voice_message_recorder.recording_in_progress:
            button_image_name = "lucide-mic-symbolic"
            toggle_image_name = "lucide-pause-symbolic"
        else:
            button_image_name = "lucide-mic-symbolic"
            toggle_image_name = "lucide-mic-symbolic"

        button_image = Gtk.Image.new_from_icon_name(button_image_name)
        self.set_child(button_image)

        self._ui.record_toggle_button_image.set_from_icon_name(toggle_image_name)

    def _voice_messages_available(self) -> bool:
        if not self._voice_message_recorder.available:
            return False

        httpupload = app.window.get_action_enabled("send-file-httpupload")
        return httpupload

    def _remove_timeout_ids(self) -> None:
        if self._time_label_update_timeout_id is not None:
            GLib.source_remove(self._time_label_update_timeout_id)
        self._time_label_update_timeout_id = None

        if self._bar_timeout_id is not None:
            GLib.source_remove(self._bar_timeout_id)
        self._bar_timeout_id = None

        if self._frame_tick_id is not None:
            self._audio_visualizer.remove_tick_callback(self._frame_tick_id)
        self._frame_tick_id = None

    def _start_recording(self) -> None:
        log.debug("Start recording")
        self._time_label_update_timeout_id = GLib.timeout_add(
            TIME_LABEL_UPDATE_DELAY,
            self._update_time_label,
            self._voice_message_recorder.recording_time,
        )

        samples = self._voice_message_recorder.recording_samples
        self._last_bar_ts = GLib.get_monotonic_time()
        self._bar_timeout_id = GLib.timeout_add(
            BAR_INTERVAL_MS, self._tick_bar, samples
        )
        self._frame_tick_id = self._audio_visualizer.add_tick_callback(self._tick_frame)

        if not self._voice_message_recorder.recording_in_progress:
            self._voice_message_recorder.start_recording()
        self._new_recording = False
        self._update_icons()

    def _stop_recording(self) -> None:
        log.debug("Stopping recording")
        self._voice_message_recorder.stop_recording()
        self._show_playback_box()
        self._remove_timeout_ids()
        self._update_icons()

    def _stop_and_reset_recording(self) -> None:
        assert app.audio_player is not None
        log.debug("Stopping and resetting recording")
        self._voice_message_recorder.stop_and_reset()

        self._ui.time_label.set_text(format_duration(0, 0))
        self._ui.send_button.set_sensitive(False)
        self._ui.cancel_button.set_sensitive(False)

        self._new_recording = True
        self._audio_visualizer.set_samples(
            self._voice_message_recorder.recording_samples()
        )
        self._audio_visualizer.render_animated_graph(0.0)
        app.audio_player.stop(self._audio_player_widget.id)

        self._show_recording_box()
        self._remove_timeout_ids()
        self._update_icons()

    def _on_error_occurred(self, occasion: int, error_msg: str) -> None:
        self._show_error_message(error_msg)

        if occasion == GST_ERROR_ON_START:
            if not self._voice_message_recorder.audio_file_is_valid:
                self._stop_and_reset_recording()
            else:
                self._stop_recording()
        elif occasion == GST_ERROR_ON_RECORDING:
            self._stop_recording()

            if not self._voice_message_recorder.audio_file_is_valid:
                self._stop_and_reset_recording()
            else:
                self._audio_player_widget.sample_voice_message(
                    self._voice_message_recorder.audio_file_abspath
                )
                self._show_playback_box()
        else:
            self._stop_and_reset_recording()

    def _update_time_label(self, recording_time: Callable[..., int]) -> bool:
        formatted = format_duration(recording_time(), recording_time())
        self._ui.time_label.set_text(formatted)
        return True

    def _tick_bar(self, samples: Callable[..., list[tuple[float, float]]]) -> bool:
        self._voice_message_recorder.request_new_sample()
        self._audio_visualizer.set_samples(samples())
        self._last_bar_ts = GLib.get_monotonic_time()
        return True

    def _tick_frame(self, _widget: Gtk.Widget, _frame_clock: Any) -> bool:
        elapsed_us = GLib.get_monotonic_time() - self._last_bar_ts
        frac = min(1.0, elapsed_us / (BAR_INTERVAL_MS * 1000))
        self._audio_visualizer.render_animated_graph(frac)
        return GLib.SOURCE_CONTINUE

    def _show_playback_box(self) -> None:
        self._ui.audio_player_box.set_visible(True)
        self._ui.progression_box.set_visible(False)

    def _show_recording_box(self) -> None:
        self._ui.audio_player_box.set_visible(False)
        self._ui.progression_box.set_visible(True)

    def _show_recording_controls(self) -> None:
        self._ui.record_control_box.set_visible(True)

    def _hide_recording_controls(self) -> None:
        self._ui.record_control_box.set_visible(False)

    def _show_error_message(self, error_msg: str) -> None:
        self._ui.error_label.set_text(error_msg)
        self._ui.error_label.set_visible(True)

    def _hide_error_message(self) -> None:
        self._ui.error_label.set_visible(False)

    def _on_record_toggle_clicked(self, _button: Gtk.Button) -> None:
        assert app.audio_player is not None
        self._hide_error_message()
        app.audio_player.stop_any()

        if self._voice_message_recorder.recording_in_progress:
            self._stop_recording()

            if not self._voice_message_recorder.audio_file_is_valid:
                log.error("Audio file is corrupted")
                return

            self._audio_player_widget.sample_voice_message(
                self._voice_message_recorder.audio_file_abspath
            )
        else:
            # Paused -> Recording
            self._hide_error_message()
            self._ui.send_button.set_sensitive(True)
            self._ui.cancel_button.set_sensitive(True)
            self._show_recording_box()
            self._start_recording()

    def _on_cancel_clicked(self, _button: Gtk.Button) -> None:
        self._stop_and_reset_recording()
        self._ui.popover.popdown()

    def _on_direct_record_begin(self, _gesture: Any, _sequence: Any) -> None:
        if not self._voice_message_recorder.recording_in_progress:
            self._voice_message_recorder.start_recording()
            self._early_recording_started = True

    def _on_direct_record_cancelled(self, _gesture: Any) -> None:
        if self._early_recording_started:
            self._early_recording_started = False
            self._voice_message_recorder.cancel_recording()

    def _on_direct_record_pressed(self, _x: float, _y: float, _user_data: Any) -> None:
        self._early_recording_started = False
        self._mic_button_long_pressed = True
        self._ui.popover.set_autohide(False)
        self._hide_recording_controls()
        self._show_recording_box()
        self._ui.popover.set_visible(True)
        self._start_recording()

    def _on_direct_record_long_press_end(self, _x: float, _y: float) -> None:
        if self._mic_button_long_pressed:
            self._mic_button_long_pressed = False
            self._ui.popover.set_autohide(True)
            self._mic_button_long_pressed = False
            self._update_icons()
            self._show_recording_controls()
            self._ui.popover.popdown()

    def _on_popover_closed(self, _popover: Gtk.Popover) -> None:
        if not self._voice_message_recorder.recording_time() > 0:
            return

        self._stop_and_reset_recording()
        path = self._voice_message_recorder.audio_file_uri
        app.window.activate_action("win.send-file", GLib.Variant("as", [path]))

    def _on_send_clicked(self, _button: Gtk.Button) -> None:
        self._stop_recording()
        path = self._voice_message_recorder.audio_file_uri
        app.window.activate_action("win.send-file", GLib.Variant("as", [path]))
