#!/usr/bin/env python3
#
# Copyright (c) 2026 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.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.
#

"""Window Title Reader plugin for Cthulhu."""

import logging

from gi.repository import GLib

from cthulhu import debug
from cthulhu import dbus_service
from cthulhu.plugin import Plugin, cthulhu_hookimpl

xlibAvailable = True
xlibImportError = None
try:
    from Xlib import X
    from Xlib import display as xDisplay
    from Xlib import error as xError
except Exception as error:
    xlibAvailable = False
    xlibImportError = error
    X = None
    xDisplay = None
    xError = None

pluginLogger = logging.getLogger(__name__)


class WindowTitleReader(Plugin):
    """Speak the active window title when toggled on."""

    def __init__(self, *positionalArgs, **keywordArgs):
        super().__init__(*positionalArgs, **keywordArgs)
        self._activated = False
        self._enabled = False
        self._pollSourceId = None
        self._pollIntervalMs = 100
        self._lastTitle = None
        self._display = None
        self._root = None
        self._atoms = {}
        self._wineDesktopLabel = "Wine Desktop"
        self._kbToggleTracking = None
        debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Plugin initialized", True)

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

        if self._activated:
            debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Already activated", True)
            return True

        self._register_keybinding()
        self._activated = True
        debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Activated", True)
        return True

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

        self._stop_tracking()
        self._activated = False
        debug.printMessage(debug.LEVEL_INFO, "WindowTitleReader: Deactivated", True)
        return True

    def _register_keybinding(self):
        if not self.app:
            debug.printMessage(
                debug.LEVEL_INFO,
                "WindowTitleReader: No app reference; cannot register keybinding",
                True,
            )
            return

        gestureString = "kb:cthulhu+control+shift+w"
        description = "Toggle window title reader"

        self._kbToggleTracking = self.registerGestureByString(
            self._toggle_tracking,
            description,
            gestureString,
        )

        if self._kbToggleTracking:
            debug.printMessage(
                debug.LEVEL_INFO,
                f"WindowTitleReader: Registered keybinding {gestureString}",
                True,
            )
        else:
            debug.printMessage(
                debug.LEVEL_INFO,
                f"WindowTitleReader: Failed to register keybinding {gestureString}",
                True,
            )

    def _toggle_tracking(self, script=None, inputEvent=None):
        if self._enabled:
            self._stop_tracking()
            self._present_message("Window title reader off")
            return True

        if not xlibAvailable:
            self._present_message("Window title reader unavailable")
            debug.printMessage(
                debug.LEVEL_INFO,
                f"WindowTitleReader: python-xlib unavailable: {xlibImportError}",
                True,
            )
            return True

        if self._start_tracking():
            self._present_message("Window title reader on")
        else:
            self._present_message("Window title reader unavailable")
        return True

    @dbus_service.parameterized_command
    def set_enabled(
        self,
        enabled: bool,
        notify_user: bool = True
    ) -> bool:
        """Enable or disable window title reader tracking."""

        if enabled:
            if self._enabled:
                if notify_user:
                    self._present_message("Window title reader on")
                return True

            if not xlibAvailable:
                if notify_user:
                    self._present_message("Window title reader unavailable")
                debug.printMessage(
                    debug.LEVEL_INFO,
                    f"WindowTitleReader: python-xlib unavailable: {xlibImportError}",
                    True,
                )
                return False

            if self._start_tracking():
                if notify_user:
                    self._present_message("Window title reader on")
                return True

            if notify_user:
                self._present_message("Window title reader unavailable")
            return False

        if self._enabled:
            self._stop_tracking()

        if notify_user:
            self._present_message("Window title reader off")
        return True

    @dbus_service.getter
    def get_enabled(self) -> bool:
        """Returns True if the window title reader is enabled."""

        return self._enabled

    def _present_message(self, messageText):
        if not self.app:
            return

        try:
            appState = self.app.getDynamicApiManager().getAPI("CthulhuState")
            if appState and appState.activeScript:
                appState.activeScript.presentMessage(messageText, resetStyles=False)
        except Exception:
            pluginLogger.exception("WindowTitleReader: Failed to present message")

    def _start_tracking(self):
        if self._enabled:
            return True

        try:
            self._display = xDisplay.Display()
            self._root = self._display.screen().root
            self._init_atoms()
            self._pollSourceId = GLib.timeout_add(self._pollIntervalMs, self._poll_window_title)
            self._enabled = True
            self._poll_window_title()
            return True
        except Exception as error:
            debug.printMessage(
                debug.LEVEL_INFO,
                f"WindowTitleReader: Failed to start tracking: {error}",
                True,
            )
            pluginLogger.exception("WindowTitleReader: Failed to start tracking")
            self._stop_tracking()
            return False

    def _stop_tracking(self):
        if self._pollSourceId is not None:
            GLib.source_remove(self._pollSourceId)
            self._pollSourceId = None

        self._enabled = False
        self._lastTitle = None
        self._cleanup_display()

    def _cleanup_display(self):
        if self._display is None:
            return

        try:
            self._display.close()
        except Exception:
            pluginLogger.exception("WindowTitleReader: Failed to close display")

        self._display = None
        self._root = None
        self._atoms = {}

    def _init_atoms(self):
        self._atoms = {
            "NET_ACTIVE_WINDOW": self._display.intern_atom("_NET_ACTIVE_WINDOW"),
            "NET_WM_NAME": self._display.intern_atom("_NET_WM_NAME"),
            "WM_NAME": self._display.intern_atom("WM_NAME"),
        }

    def _poll_window_title(self):
        if not self._enabled:
            return False

        try:
            activeWindow = self._get_active_window()
            if not activeWindow:
                self._lastTitle = None
                return True

            windowTitle = self._get_current_title(activeWindow)
            if not windowTitle:
                self._lastTitle = None
                return True

            if windowTitle != self._lastTitle:
                self._present_title(windowTitle)
                self._lastTitle = windowTitle
        except Exception as error:
            debug.printMessage(debug.LEVEL_INFO, f"WindowTitleReader: Poll error: {error}", True)
            pluginLogger.exception("WindowTitleReader: Poll failed")

        return True

    def _present_title(self, titleText):
        if not self.app:
            return

        try:
            appState = self.app.getDynamicApiManager().getAPI("CthulhuState")
            if appState and appState.activeScript:
                appState.activeScript.presentMessage(titleText, resetStyles=False)
        except Exception:
            pluginLogger.exception("WindowTitleReader: Failed to present title")

    def _get_current_title(self, activeWindow):
        activeTitle = self._get_window_title(activeWindow)
        if not activeTitle:
            return ""

        if self._is_wine_desktop_title(activeTitle):
            return self._get_wine_desktop_title(activeWindow)

        return activeTitle

    def _get_wine_desktop_title(self, desktopWindow):
        focusWindow = self._get_focus_window()
        if focusWindow and focusWindow.id != desktopWindow.id:
            focusTitle = self._get_window_title(focusWindow)
            if focusTitle and not self._is_wine_desktop_title(focusTitle):
                return focusTitle

        return self._find_child_title(desktopWindow)

    def _find_child_title(self, rootWindow):
        try:
            childQueue = list(rootWindow.query_tree().children)
        except xError.XError:
            return ""

        checkedCount = 0
        while childQueue and checkedCount < 200:
            window = childQueue.pop(0)
            checkedCount += 1
            titleText = self._get_window_title(window)
            if titleText and not self._is_wine_desktop_title(titleText):
                return titleText

            try:
                childQueue.extend(window.query_tree().children)
            except xError.XError:
                continue

        return ""

    def _is_wine_desktop_title(self, titleText):
        return self._wineDesktopLabel.lower() in titleText.lower()

    def _get_window_title(self, window):
        if not window:
            return ""

        netTitle = self._get_text_property(window, "NET_WM_NAME")
        if netTitle:
            return netTitle

        return self._get_text_property(window, "WM_NAME")

    def _get_text_property(self, window, atomName):
        atom = self._atoms.get(atomName)
        if not atom:
            return ""

        try:
            propertyData = window.get_full_property(atom, X.AnyPropertyType)
        except xError.XError:
            return ""

        if not propertyData or propertyData.value is None:
            return ""

        value = propertyData.value
        if isinstance(value, str):
            return value.strip("\x00")

        try:
            rawBytes = bytes(value)
        except Exception:
            return ""

        decodedText = self._decode_property_bytes(rawBytes)
        return decodedText.strip("\x00")

    def _decode_property_bytes(self, rawBytes):
        for encoding in ("utf-8", "latin-1"):
            try:
                return rawBytes.decode(encoding, errors="replace")
            except Exception:
                continue
        return ""

    def _get_active_window(self):
        if not self._root:
            return None

        try:
            propertyData = self._root.get_full_property(
                self._atoms["NET_ACTIVE_WINDOW"],
                X.AnyPropertyType,
            )
        except xError.XError:
            propertyData = None

        if propertyData and propertyData.value:
            try:
                windowId = int(propertyData.value[0])
                return self._display.create_resource_object("window", windowId)
            except Exception:
                pass

        return self._get_focus_window()

    def _get_focus_window(self):
        if not self._display:
            return None

        try:
            focusInfo = self._display.get_input_focus()
            focusWindow = focusInfo.focus
            if not focusWindow or focusWindow.id == 0:
                return None
            if self._root and focusWindow.id == self._root.id:
                return None
            return focusWindow
        except Exception:
            return None
