#!/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.

"""Adapter that normalizes compositor and AT-SPI desktop context signals."""

from __future__ import annotations

from dataclasses import replace
from collections.abc import Callable, Iterable
from typing import Any, Optional

from . import cthulhu_state
from .ax_object import AXObject
from .compositor_state_types import (
    CompositorStateEvent,
    DesktopContextSnapshot,
    DESKTOP_TRANSITION_FINISHED,
    DESKTOP_TRANSITION_STARTED,
    DESKTOP_FOCUS_CONTEXT_CHANGED,
    FLUSH_STALE_ATSPI_EVENTS,
    PAUSE_ATSPI_CHURN,
    PRIORITIZE_FOCUS,
    RESUME_ATSPI_CHURN,
    WORKSPACE_STATE_CHANGED,
)
from .compositor_state_wayland import NullWorkspaceBackend, WaylandSharedProtocolsBackend
from .wnck_support import get_session_type


class CompositorStateAdapter:
    """Normalizes compositor state and desktop-focus context changes."""

    def __init__(
        self,
        workspace_backends: Optional[Iterable[Any]] = None,
    ) -> None:
        if workspace_backends is None:
            workspace_backends = [
                WaylandSharedProtocolsBackend(),
                NullWorkspaceBackend(),
            ]
        self._workspaceBackends = list(workspace_backends)
        self._workspaceBackend: Optional[Any] = None
        self._listeners: list[Callable[[CompositorStateEvent], None]] = []
        self._snapshot = DesktopContextSnapshot(session_type=self.get_session_type())

    def add_listener(self, listener: Callable[[CompositorStateEvent], None]) -> None:
        if listener not in self._listeners:
            self._listeners.append(listener)

    def remove_listener(self, listener: Callable[[CompositorStateEvent], None]) -> None:
        if listener in self._listeners:
            self._listeners.remove(listener)

    def get_snapshot(self) -> DesktopContextSnapshot:
        return self._snapshot

    def get_session_type(self) -> str:
        return get_session_type()

    def activate(self) -> None:
        self._deactivate_current_backend()
        for backend in self._workspaceBackends:
            if not self._backend_is_available(backend):
                continue
            self._workspaceBackend = backend
            self._backend_activate(backend)
            break

        self._snapshot = DesktopContextSnapshot(
            session_type=self.get_session_type(),
            backend_name=self._backend_name(),
        )
        self.sync_accessible_context("activate")

    def deactivate(self) -> None:
        self._deactivate_current_backend()
        self._snapshot = DesktopContextSnapshot(session_type=self.get_session_type())
        cthulhu_state.compositorSnapshot = None
        cthulhu_state.pauseAtspiChurn = False
        cthulhu_state.prioritizedDesktopContextToken = None

    def sync_accessible_context(self, reason: str) -> DesktopContextSnapshot:
        active_window = cthulhu_state.activeWindow
        locus_of_focus = cthulhu_state.locusOfFocus

        active_window_snapshot = self._build_object_snapshot(active_window)
        locus_of_focus_snapshot = self._build_object_snapshot(locus_of_focus)
        workspaceSnapshot = self._snapshot
        snapshot = DesktopContextSnapshot(
            session_type=self.get_session_type(),
            backend_name=self._backend_name(),
            active_workspace_ids=workspaceSnapshot.active_workspace_ids,
            workspace_transition_pending=workspaceSnapshot.workspace_transition_pending,
            active_window_token=active_window_snapshot["token"],
            locus_of_focus_token=locus_of_focus_snapshot["token"],
            active_window_pid=active_window_snapshot["pid"],
            locus_of_focus_pid=locus_of_focus_snapshot["pid"],
            active_window_name=active_window_snapshot["name"],
            locus_of_focus_name=locus_of_focus_snapshot["name"],
        )

        previous_token = self._snapshot.active_window_token
        self._snapshot = snapshot
        cthulhu_state.compositorSnapshot = snapshot

        if snapshot.active_window_token != previous_token:
            cthulhu_state.prioritizedDesktopContextToken = snapshot.active_window_token or None
            self._emit(
                DESKTOP_FOCUS_CONTEXT_CHANGED,
                reason,
                snapshot,
            )
            self._emit(
                PRIORITIZE_FOCUS,
                reason,
                snapshot,
            )

        return snapshot

    def _build_object_snapshot(self, obj: Any) -> dict[str, Any]:
        if obj is None:
            return {"token": "", "pid": -1, "name": ""}

        pid = AXObject.get_process_id(obj)
        name = (AXObject.get_name(obj) or "").strip()
        if not name:
            name = AXObject.get_role_name(obj) or AXObject.get_accessible_id(obj) or "unknown"

        token = ""
        if pid >= 0 or name:
            token = f"{max(pid, 0)}:{name}"

        return {"token": token, "pid": pid, "name": name}

    def _handle_workspace_signal(
        self,
        signal_name: str,
        workspace_ids: Optional[Iterable[str]] = None,
        reason: str = "",
    ) -> None:
        if workspace_ids is not None:
            snapshot = replace(
                self._snapshot,
                active_workspace_ids=frozenset(str(workspaceId) for workspaceId in workspace_ids if workspaceId),
                workspace_transition_pending=signal_name == DESKTOP_TRANSITION_STARTED,
            )
            self._snapshot = snapshot
            cthulhu_state.compositorSnapshot = snapshot

            if signal_name == DESKTOP_TRANSITION_STARTED:
                cthulhu_state.pauseAtspiChurn = True
                self._emit(signal_name, reason, snapshot)
                self._emit(PAUSE_ATSPI_CHURN, reason, snapshot)
                return

            if signal_name == DESKTOP_TRANSITION_FINISHED:
                cthulhu_state.pauseAtspiChurn = False
                self._emit(signal_name, reason, snapshot)
                self._emit(RESUME_ATSPI_CHURN, reason, snapshot)
                self._emit(FLUSH_STALE_ATSPI_EVENTS, reason, snapshot)
                return

            if signal_name == PAUSE_ATSPI_CHURN:
                cthulhu_state.pauseAtspiChurn = True
            elif signal_name == RESUME_ATSPI_CHURN:
                cthulhu_state.pauseAtspiChurn = False

            self._emit(signal_name, reason, snapshot)
            return

        normalized = (signal_name or "").strip().lower()
        if not normalized:
            return

        if "transition" not in normalized and "workspace" not in normalized:
            return

        if any(marker in normalized for marker in ("start", "begin", "enter", "prepare", "opening")):
            self._handle_workspace_signal(
                DESKTOP_TRANSITION_STARTED,
                self._snapshot.active_workspace_ids,
                signal_name,
            )
            return

        if any(marker in normalized for marker in ("end", "complete", "finished", "finish", "leave", "exit", "stop", "done")):
            self._handle_workspace_signal(
                DESKTOP_TRANSITION_FINISHED,
                self._snapshot.active_workspace_ids,
                signal_name,
            )
            return

        if "flush" in normalized or "stale" in normalized:
            self._emit(FLUSH_STALE_ATSPI_EVENTS, signal_name, self._snapshot)
            return

        if "workspace" in normalized:
            self._handle_workspace_signal(
                WORKSPACE_STATE_CHANGED,
                self._snapshot.active_workspace_ids,
                signal_name,
            )

    def _emit(
        self,
        event_type: str,
        reason: str,
        snapshot: Optional[DesktopContextSnapshot],
        payload: Optional[dict[str, Any]] = None,
    ) -> None:
        event = CompositorStateEvent(event_type, reason=reason, snapshot=snapshot, payload=payload or {})
        for listener in list(self._listeners):
            listener(event)

    def _backend_is_available(self, backend: Any) -> bool:
        if hasattr(backend, "is_available"):
            try:
                return bool(backend.is_available(self.get_session_type()))
            except TypeError:
                return bool(backend.is_available())
        if hasattr(backend, "available"):
            return bool(backend.available)
        return True

    def _backend_activate(self, backend: Any) -> None:
        if hasattr(backend, "activate"):
            try:
                backend.activate(self._handle_workspace_signal)
            except TypeError:
                try:
                    backend.activate(self)
                except TypeError:
                    backend.activate()

    def _backend_deactivate(self, backend: Any) -> None:
        if hasattr(backend, "deactivate"):
            try:
                backend.deactivate(self)
            except TypeError:
                backend.deactivate()

    def _deactivate_current_backend(self) -> None:
        if self._workspaceBackend is None:
            return

        self._backend_deactivate(self._workspaceBackend)
        self._workspaceBackend = None

    def _backend_name(self) -> str:
        if self._workspaceBackend is None:
            return ""

        name = getattr(self._workspaceBackend, "name", "")
        if name:
            return str(name)

        return self._workspaceBackend.__class__.__name__
