#!/usr/bin/env python3
#
# Copyright (c) 2026 Stormux
# Copyright (c) 2023 Igalia, S.L.
# Author: Joanmarie Diggs <jdiggs@igalia.com>
#
# 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.
#
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu

"""Manages the Cthulhu modifier key(s)."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2026 Stormux"
__license__   = "LGPL"

import os
import re
import subprocess

import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from gi.repository import GLib

import cthulhu.debug as debug
import cthulhu.settings as settings

class CthulhuModifierManager:
    """Manages the Cthulhu modifier."""

    def __init__(self):
        self._originalXmodmap = b""
        self._capsLockCleared = False

    def refreshCthulhuModifiers(self, reason=""):
        """Refreshes the Cthulhu modifier keys."""

        msg = "CTHULHU MODIFIER MANAGER: Refreshing Cthulhu modifiers"
        if reason:
            msg += f": {reason}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        display = os.environ.get("DISPLAY")
        if not display:
            msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xkbcomp operations"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        self.unsetCthulhuModifiers(reason)
        with subprocess.Popen(["xkbcomp", display, "-"],
                              stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) as process:
            self._originalXmodmap, _ = process.communicate()
        self._createCthulhuXmodmap()

    def unsetCthulhuModifiers(self, reason=""):
        """Turns the Cthulhu modifiers back into their original purpose."""

        msg = "CTHULHU MODIFIER MANAGER: Attempting to restore original xmodmap"
        if reason:
            msg += f": {reason}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        if not self._originalXmodmap:
            msg = "CTHULHU MODIFIER MANAGER: No stored xmodmap found"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        display = os.environ.get("DISPLAY")
        if not display:
            msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xmodmap restoration"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        self._capsLockCleared = False
        with subprocess.Popen(["xkbcomp", "-w0", "-", display],
                              stdin=subprocess.PIPE, stdout=None, stderr=None) as process:
            process.communicate(self._originalXmodmap)

        msg = "CTHULHU MODIFIER MANAGER: Original xmodmap restored"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _createCthulhuXmodmap(self):
        """Makes a Cthulhu-specific Xmodmap so that the modifier works."""

        msg = "CTHULHU MODIFIER MANAGER: Creating Cthulhu xmodmap"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        if "Caps_Lock" in settings.cthulhuModifierKeys \
           or "Shift_Lock" in settings.cthulhuModifierKeys:
            self.setCapsLockAsCthulhuModifier(True)
            self._capsLockCleared = True
        elif self._capsLockCleared:
            self.setCapsLockAsCthulhuModifier(False)
            self._capsLockCleared = False

    def setCapsLockAsCthulhuModifier(self, enable):
        """Enable or disable use of the caps lock key as a Cthulhu modifier key."""

        msg = "CTHULHU MODIFIER MANAGER: Setting caps lock as the Cthulhu modifier"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        display = os.environ.get("DISPLAY")
        if not display:
            msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, cannot modify caps lock"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if not self._originalXmodmap:
            msg = "CTHULHU MODIFIER MANAGER: No xmodmap available, cannot modify caps lock"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        interpretCapsLineProg = re.compile(
            r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
        normalCapsLineProg = re.compile(
            r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
        interpretShiftLineProg = re.compile(
            r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
        normalShiftLineProg = re.compile(
            r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
        disabledModLineProg = re.compile(
            r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
        normalCapsLine = '        action= LockMods(modifiers=Lock);'
        normalShiftLine = '        action= LockMods(modifiers=Shift);'
        disabledModLine = '        action= NoAction();'
        lines = self._originalXmodmap.decode('UTF-8').split('\n')
        foundCapsInterpretSection = False
        foundShiftInterpretSection = False
        modified = False
        for i, line in enumerate(lines):
            if not foundCapsInterpretSection and not foundShiftInterpretSection:
                if interpretCapsLineProg.match(line):
                    foundCapsInterpretSection = True
                elif interpretShiftLineProg.match(line):
                    foundShiftInterpretSection = True
            elif foundCapsInterpretSection:
                if enable:
                    if normalCapsLineProg.match(line):
                        lines[i] = disabledModLine
                        modified = True
                else:
                    if disabledModLineProg.match(line):
                        lines[i] = normalCapsLine
                        modified = True
                if line.find('}'):
                    foundCapsInterpretSection = False
            elif foundShiftInterpretSection:
                if enable:
                    if normalShiftLineProg.match(line):
                        lines[i] = disabledModLine
                        modified = True
                else:
                    if disabledModLineProg.match(line):
                        lines[i] = normalShiftLine
                        modified = True
                if line.find('}'):
                    foundShiftInterpretSection = False
        if modified:
            msg = "CTHULHU MODIFIER MANAGER: Updating xmodmap"
            debug.printMessage(debug.LEVEL_INFO, msg, True)

            with subprocess.Popen(["xkbcomp", "-w0", "-", display],
                                  stdin=subprocess.PIPE, stdout=None, stderr=None) as process:
                process.communicate(bytes('\n'.join(lines), 'UTF-8'))
        else:
            msg = "CTHULHU MODIFIER MANAGER: Not updating xmodmap"
            debug.printMessage(debug.LEVEL_INFO, msg, True)

    def toggleModifier(self, keyboardEvent):
        """Toggles the modifier to enable double-clicking causing normal behavior."""

        if keyboardEvent.keyval_name in ["Caps_Lock", "Shift_Lock"]:
            self._toggleModifierLock(keyboardEvent)
            return

    def _toggleModifierLock(self, keyboardEvent):
        """Toggles the lock for a modifier to enable double-clicking causing normal behavior."""

        if not keyboardEvent.is_pressed_key():
            return

        def toggle(modifiers, modifier):
            if modifiers & modifier:
                lock = Atspi.KeySynthType.UNLOCKMODIFIERS
                msg = "CTHULHU MODIFIER MANAGER: Unlocking CapsLock"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
            else:
                lock = Atspi.KeySynthType.LOCKMODIFIERS
                msg = "CTHULHU MODIFIER MANAGER: Locking CapsLock"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
            Atspi.generate_keyboard_event(modifier, "", lock)

        if keyboardEvent.keyval_name == "Caps_Lock":
            modifier = 1 << Atspi.ModifierType.SHIFTLOCK
        elif keyboardEvent.keyval_name == "Shift_Lock":
            modifier = 1 << Atspi.ModifierType.SHIFT
        else:
            return

        msg = "CTHULHU MODIFIER MANAGER: Scheduling lock change"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        GLib.timeout_add(1, toggle, keyboardEvent.modifiers, modifier)

_manager = None
def getManager():
    """Returns the Cthulhu Modifier Manager"""

    global _manager
    if _manager is None:
        _manager = CthulhuModifierManager()
    return _manager

