#!/usr/bin/env python3
#
# Copyright (c) 2025 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.

"""Speech history and speech monitor plugin for Cthulhu."""

import logging

import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib

from cthulhu.plugin import Plugin, cthulhu_hookimpl
from cthulhu import debug
from cthulhu import speech
from cthulhu import speech_history

logger = logging.getLogger(__name__)


class SpeechHistory(Plugin):
    """Plugin that provides speech history and a live speech monitor."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._activated = False
        self._kbOpenWindow = None
        self._kbToggleMonitor = None

        # Speech history window state
        self._window = None
        self._filterEntry = None
        self._filterText = ""
        self._listStore = None
        self._filterModel = None
        self._treeView = None
        self._capturePaused = False

        # Live monitor window state
        self._monitorWindow = None
        self._monitorTextView = None
        self._monitorTextBuffer = None
        self._monitorEndMark = None
        self._monitorLineCount = 0
        self._monitorMaxLines = 500

    @cthulhu_hookimpl
    def activate(self, plugin=None):
        if plugin is not None and plugin is not self:
            return

        if self._activated:
            return True

        try:
            self._register_keybindings()
            self._activated = True
            return True
        except Exception:
            logger.exception("Error activating SpeechHistory plugin")
            return False

    @cthulhu_hookimpl
    def deactivate(self, plugin=None):
        if plugin is not None and plugin is not self:
            return

        try:
            self._close_window()
            self._disable_monitor()
            self._kbOpenWindow = None
            self._kbToggleMonitor = None
            self._activated = False
            return True
        except Exception:
            logger.exception("Error deactivating SpeechHistory plugin")
            return False

    def _register_keybindings(self):
        if not self.app:
            debug.printMessage(
                debug.LEVEL_INFO,
                "SpeechHistory: No app reference; cannot register keybindings",
                True,
            )
            return

        historyGesture = "kb:cthulhu+control+h"
        historyDescription = "Open speech history"
        self._kbOpenWindow = self.registerGestureByString(
            self._open_window,
            historyDescription,
            historyGesture,
        )

        monitorGesture = "kb:cthulhu+shift+d"
        monitorDescription = "Toggle speech monitor"
        self._kbToggleMonitor = self.registerGestureByString(
            self._toggle_monitor,
            monitorDescription,
            monitorGesture,
        )

        if not self._kbOpenWindow:
            debug.printMessage(
                debug.LEVEL_INFO,
                f"SpeechHistory: Failed to register keybinding {historyGesture}",
                True,
            )

        if not self._kbToggleMonitor:
            debug.printMessage(
                debug.LEVEL_INFO,
                f"SpeechHistory: Failed to register keybinding {monitorGesture}",
                True,
            )

    # Live monitor methods

    def _toggle_monitor(self, script=None, inputEvent=None):
        try:
            if self._monitorWindow is not None:
                self._disable_monitor()
                self._present_message("Speech monitor disabled.")
                return True

            self._enable_monitor()
            self._present_message("Speech monitor enabled.")
            return True
        except Exception:
            logger.exception("Error toggling speech monitor")
            self._disable_monitor()
            self._present_message("Error toggling speech monitor.")
            return False

    def _enable_monitor(self):
        if self._monitorWindow is not None:
            self._monitorWindow.show_all()
            return

        self._create_monitor_window()
        speech.set_monitor_callbacks(writeText=self._on_spoken_text)
        self._monitorWindow.show_all()

    def _disable_monitor(self):
        speech.set_monitor_callbacks(writeText=None)

        if self._monitorWindow is not None:
            self._monitorWindow.destroy()

    def _create_monitor_window(self):
        self._monitorWindow = Gtk.Window(title="Speech Monitor - Cthulhu")
        self._monitorWindow.set_default_size(900, 320)
        self._monitorWindow.set_modal(False)
        self._monitorWindow.set_border_width(8)
        self._monitorWindow.set_keep_above(True)
        self._monitorWindow.set_accept_focus(False)
        self._monitorWindow.set_focus_on_map(False)
        self._monitorWindow.set_skip_taskbar_hint(True)
        self._monitorWindow.set_skip_pager_hint(True)
        self._monitorWindow.set_type_hint(Gdk.WindowTypeHint.UTILITY)

        self._monitorWindow.connect("destroy", self._on_monitor_window_destroy)
        self._monitorWindow.connect("key-press-event", self._on_monitor_window_key_press)

        mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)

        scrolledWindow = Gtk.ScrolledWindow()
        scrolledWindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

        self._monitorTextView = Gtk.TextView()
        self._monitorTextView.set_editable(False)
        self._monitorTextView.set_cursor_visible(False)
        self._monitorTextView.set_can_focus(False)
        self._monitorTextView.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
        self._monitorTextView.set_accepts_tab(False)
        self._monitorTextView.set_left_margin(6)
        self._monitorTextView.set_right_margin(6)
        self._monitorTextView.set_top_margin(6)
        self._monitorTextView.set_bottom_margin(6)

        textAccessible = self._monitorTextView.get_accessible()
        if textAccessible:
            textAccessible.set_name("Speech monitor output")

        self._monitorTextBuffer = self._monitorTextView.get_buffer()
        self._monitorEndMark = self._monitorTextBuffer.create_mark(
            "monitorEnd",
            self._monitorTextBuffer.get_end_iter(),
            False,
        )
        self._monitorLineCount = 0

        scrolledWindow.add(self._monitorTextView)
        mainBox.pack_start(scrolledWindow, True, True, 0)

        buttonRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        buttonRow.set_halign(Gtk.Align.END)

        clearButton = Gtk.Button(label="Clear")
        closeButton = Gtk.Button(label="Close")
        clearButton.connect("clicked", self._on_monitor_clear_clicked)
        closeButton.connect("clicked", self._on_monitor_close_clicked)

        buttonRow.pack_start(clearButton, False, False, 0)
        buttonRow.pack_start(closeButton, False, False, 0)
        mainBox.pack_start(buttonRow, False, False, 0)

        self._monitorWindow.add(mainBox)

    def _on_monitor_close_clicked(self, button):
        self._disable_monitor()

    def _on_monitor_clear_clicked(self, button):
        if self._monitorTextBuffer is None:
            return

        self._monitorTextBuffer.set_text("")
        self._monitorLineCount = 0

    def _on_monitor_window_destroy(self, widget):
        speech.set_monitor_callbacks(writeText=None)

        self._monitorWindow = None
        self._monitorTextView = None
        self._monitorTextBuffer = None
        self._monitorEndMark = None
        self._monitorLineCount = 0

    def _on_monitor_window_key_press(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self._disable_monitor()
            return True

        return False

    def _on_spoken_text(self, text):
        if not text or not isinstance(text, str):
            return

        GLib.idle_add(self._append_text_on_main_thread, text)

    def _append_text_on_main_thread(self, text):
        if self._monitorTextBuffer is None:
            return False

        self._monitorTextBuffer.insert(self._monitorTextBuffer.get_end_iter(), f"{text}\n")
        self._monitorLineCount += 1

        if self._monitorLineCount > self._monitorMaxLines:
            excess = self._monitorLineCount - self._monitorMaxLines
            startIter = self._monitorTextBuffer.get_start_iter()
            cutIter = self._monitorTextBuffer.get_iter_at_line(excess)
            self._monitorTextBuffer.delete(startIter, cutIter)
            self._monitorLineCount = self._monitorMaxLines

        if self._monitorEndMark is not None and self._monitorTextView is not None:
            self._monitorTextBuffer.move_mark(
                self._monitorEndMark,
                self._monitorTextBuffer.get_end_iter(),
            )
            self._monitorTextView.scroll_mark_onscreen(self._monitorEndMark)

        return False

    # Speech history methods

    def _open_window(self, script=None, inputEvent=None):
        try:
            if self._window:
                self._window.present()
                return True

            self._pause_capture()

            self._create_window()
            self._window.show_all()
            if self._treeView and len(self._filterModel) > 0:
                self._treeView.grab_focus()
                self._treeView.set_cursor(Gtk.TreePath.new_first())
            elif self._filterEntry:
                self._filterEntry.grab_focus()

            return True
        except Exception:
            logger.exception("Error opening SpeechHistory window")
            self._resume_capture()
            return False

    def _create_window(self):
        self._window = Gtk.Window(title="Speech History - Cthulhu")
        self._window.set_default_size(700, 420)
        self._window.set_modal(True)
        self._window.set_border_width(10)

        self._window.connect("destroy", self._on_window_destroy)
        self._window.connect("key-press-event", self._on_window_key_press)

        mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)

        self._filterText = ""

        filterRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        filterLabel = Gtk.Label(label="_Filter:")
        filterLabel.set_use_underline(True)
        filterLabel.set_halign(Gtk.Align.START)

        self._filterEntry = Gtk.Entry()
        self._filterEntry.set_hexpand(True)
        filterLabel.set_mnemonic_widget(self._filterEntry)
        self._filterEntry.connect("changed", self._on_filter_changed)

        filterRow.pack_start(filterLabel, False, False, 0)
        filterRow.pack_start(self._filterEntry, True, True, 0)
        mainBox.pack_start(filterRow, False, False, 0)

        self._listStore = Gtk.ListStore(str)
        self._filterModel = self._listStore.filter_new()
        self._filterModel.set_visible_func(self._filter_visible_func)

        self._treeView = Gtk.TreeView(model=self._filterModel)
        self._treeView.set_headers_visible(True)

        selection = self._treeView.get_selection()
        selection.set_mode(Gtk.SelectionMode.SINGLE)

        textRenderer = Gtk.CellRendererText()
        textRenderer.set_property("wrap-width", 640)
        textRenderer.set_property("wrap-mode", 2)
        textColumn = Gtk.TreeViewColumn("Spoken Text", textRenderer, text=0)
        textColumn.set_resizable(True)
        textColumn.set_expand(True)
        self._treeView.append_column(textColumn)

        scrolled = Gtk.ScrolledWindow()
        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolled.add(self._treeView)
        mainBox.pack_start(scrolled, True, True, 0)

        buttonRow = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        buttonRow.set_halign(Gtk.Align.END)

        copyButton = Gtk.Button(label="Copy to clipboard")
        removeButton = Gtk.Button(label="Remove from history")
        closeButton = Gtk.Button(label="Close")

        copyButton.connect("clicked", self._on_copy_clicked)
        removeButton.connect("clicked", self._on_remove_clicked)
        closeButton.connect("clicked", self._on_close_clicked)

        buttonRow.pack_start(copyButton, False, False, 0)
        buttonRow.pack_start(removeButton, False, False, 0)
        buttonRow.pack_start(closeButton, False, False, 0)
        mainBox.pack_start(buttonRow, False, False, 0)

        self._window.add(mainBox)

        self._refresh_list(selectFirst=True)

    def _on_filter_changed(self, entry):
        try:
            self._filterText = entry.get_text() or ""
            if self._filterModel:
                self._filterModel.refilter()
        except Exception:
            logger.exception("Error updating speech history filter")

    def _filter_visible_func(self, model, treeIter, data=None):
        try:
            filterText = (self._filterText or "").strip().lower()
            if not filterText:
                return True

            spokenText = model[treeIter][0] or ""
            return spokenText.lower().startswith(filterText)
        except Exception:
            return True

    def _refresh_list(self, selectFirst=False):
        try:
            if not self._listStore:
                return

            self._listStore.clear()

            items = speech_history.get_items()
            for item in items:
                self._listStore.append([item])

            if self._filterModel:
                self._filterModel.refilter()

            if selectFirst and self._treeView and len(self._filterModel) > 0:
                selection = self._treeView.get_selection()
                selection.select_path(Gtk.TreePath.new_first())
        except Exception:
            logger.exception("Error refreshing speech history list")

    def _get_selected_text(self):
        try:
            if not self._treeView:
                return None

            selection = self._treeView.get_selection()
            model, treeIter = selection.get_selected()
            if not treeIter:
                return None

            return model[treeIter][0]
        except Exception:
            logger.exception("Error getting selected speech history item")
            return None

    def _on_copy_clicked(self, button):
        try:
            selectedText = self._get_selected_text()
            if not selectedText:
                self._present_message("No speech history item selected.")
                return

            clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
            clipboard.set_text(selectedText, -1)
            clipboard.store()

            self._present_message("Copied to clipboard.")
        except Exception:
            logger.exception("Error copying speech history item to clipboard")
            self._present_message("Error copying to clipboard.")

    def _on_remove_clicked(self, button):
        try:
            selectedText = self._get_selected_text()
            if not selectedText:
                self._present_message("No speech history item selected.")
                return

            removed = speech_history.remove(selectedText)
            if removed:
                self._refresh_list(selectFirst=True)
                self._present_message("Removed from history.")
            else:
                self._present_message("Item not found in history.")
        except Exception:
            logger.exception("Error removing speech history item")
            self._present_message("Error removing item from history.")

    def _on_close_clicked(self, button):
        self._close_window()

    def _close_window(self):
        try:
            if self._window:
                self._window.destroy()
        except Exception:
            logger.exception("Error closing SpeechHistory window")
            self._resume_capture()

    def _on_window_destroy(self, widget):
        self._window = None
        self._filterEntry = None
        self._listStore = None
        self._filterModel = None
        self._treeView = None
        self._filterText = ""
        self._resume_capture()

    def _on_window_key_press(self, widget, event):
        try:
            if event.keyval == Gdk.KEY_Escape:
                self._close_window()
                return True

            if not self._filterEntry or self._filterEntry.is_focus():
                return False

            if event.keyval == Gdk.KEY_BackSpace:
                currentText = self._filterEntry.get_text() or ""
                if currentText:
                    self._filterEntry.set_text(currentText[:-1])
                    self._filterEntry.set_position(-1)
                    return True
                return False

            modifierMask = (
                Gdk.ModifierType.CONTROL_MASK
                | Gdk.ModifierType.MOD1_MASK
                | Gdk.ModifierType.SUPER_MASK
                | Gdk.ModifierType.META_MASK
            )
            if event.state & modifierMask:
                return False

            keyUnicode = Gdk.keyval_to_unicode(event.keyval)
            if not keyUnicode:
                return False

            charTyped = chr(keyUnicode)
            if not charTyped.isprintable():
                return False

            self._filterEntry.grab_focus()
            currentText = self._filterEntry.get_text() or ""
            self._filterEntry.set_text(currentText + charTyped)
            self._filterEntry.set_position(-1)
            return True
        except Exception:
            logger.exception("Error handling key press in SpeechHistory window")
            return False

    def _pause_capture(self):
        if self._capturePaused:
            return

        speech_history.pause_capture(reason="SpeechHistory window open")
        self._capturePaused = True

    def _resume_capture(self):
        if not self._capturePaused:
            return

        speech_history.resume_capture(reason="SpeechHistory window closed")
        self._capturePaused = False

    def _present_message(self, message):
        try:
            if not self.app:
                debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: {message}", True)
                return

            state = self.app.getDynamicApiManager().getAPI("CthulhuState")
            if state and state.activeScript:
                state.activeScript.presentMessage(message, resetStyles=False)
            else:
                debug.printMessage(debug.LEVEL_INFO, f"SpeechHistory: {message}", True)
        except Exception:
            logger.exception("Error presenting message from SpeechHistory")
