#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
# Copyright (c) 2024 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

"""Module for sleep mode management."""

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

import time
import os
from gi.repository import GLib
from tomlkit import parse
import cthulhu.braille as braille
import cthulhu.cmdnames as cmdnames
import cthulhu.debug as debug
import cthulhu.input_event as input_event
import cthulhu.keybindings as keybindings
import cthulhu.messages as messages
import cthulhu.script_manager as script_manager
from cthulhu.ax_object import AXObject

class SleepModeManager:
    """Provides sleep mode implementation."""

    def __init__(self):
        self._handlers = self.getHandlers(True)
        self._bindings = keybindings.KeyBindings()
        self._apps = set()
        self._disabledAutoSleepApps = set()
        self._autoSleepAppNames = set()
        self._autoSleepPath = self._getAutoSleepPath()
        self._autoSleepConfigMTime = None
        self._loadAutoSleepConfig()
        self._lastToggleTime = 0
        self._toggleDebounceDelay = 0.1  # 100ms debounce (reduced for better responsiveness)

    def getBindings(self, refresh=False, isDesktop=True):
        """Returns the sleep-mode-manager keybindings."""

        if refresh:
            msg = f"SLEEP MODE MANAGER: Refreshing bindings. Is desktop: {isDesktop}"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self._setupBindings()
        elif len(self._bindings.keyBindings) == 0:
            self._setupBindings()

        return self._bindings

    def getHandlers(self, refresh=False):
        """Returns the sleep-mode-manager handlers."""

        if refresh:
            msg = "SLEEP MODE MANAGER: Refreshing handlers."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self._setupHandlers()

        return self._handlers

    def isActiveForApp(self, app):
        """Returns True if sleep mode is active for app."""

        if not app:
            return False

        appHash = hash(app)
        result = appHash in self._apps
        if not result and self._isAutoSleepConfiguredForApp(app):
            result = appHash not in self._disabledAutoSleepApps

        if result:
            tokens = ["SLEEP MODE MANAGER: Is active for", app]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
        return result

    def _getAutoSleepPath(self):
        prefsDir = os.path.join(GLib.get_user_data_dir(), "cthulhu")
        try:
            from . import cthulhu
            app = cthulhu.cthulhuApp
            if app and app.settingsManager:
                configuredDir = app.settingsManager.getPrefsDir()
                if configuredDir:
                    prefsDir = configuredDir
        except Exception:
            pass
        return os.path.join(prefsDir, "sleep.toml")

    def _refreshAutoSleepPath(self):
        latestPath = self._getAutoSleepPath()
        if latestPath != self._autoSleepPath:
            self._autoSleepPath = latestPath
            self._autoSleepConfigMTime = None
            self._loadAutoSleepConfig()
            return

        try:
            latestMTime = os.path.getmtime(self._autoSleepPath)
        except OSError:
            latestMTime = None

        if latestMTime != self._autoSleepConfigMTime:
            self._loadAutoSleepConfig()

    def refreshAutoSleepConfig(self):
        """Refresh auto-sleep config if prefs directory has changed."""

        self._refreshAutoSleepPath()

    def _loadAutoSleepConfig(self):
        self._autoSleepAppNames = set()
        self._autoSleepConfigMTime = None

        if not os.path.isfile(self._autoSleepPath):
            msg = f"SLEEP MODE MANAGER: No sleep config at {self._autoSleepPath}"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        try:
            self._autoSleepConfigMTime = os.path.getmtime(self._autoSleepPath)
        except OSError:
            self._autoSleepConfigMTime = None

        try:
            with open(self._autoSleepPath, "r", encoding="utf-8") as configFile:
                config = parse(configFile.read() or "")
        except Exception as error:
            tokens = ["SLEEP MODE MANAGER: Failed to parse", self._autoSleepPath, ":", error]
            debug.printTokens(debug.LEVEL_WARNING, tokens, True)
            return

        appNames = []
        topLevelApps = config.get("apps", [])
        if isinstance(topLevelApps, list):
            appNames.extend(topLevelApps)

        sleepSection = config.get("sleep", {})
        if isinstance(sleepSection, dict):
            sectionApps = sleepSection.get("apps", [])
            if isinstance(sectionApps, list):
                appNames.extend(sectionApps)

        for appName in appNames:
            if not isinstance(appName, str):
                continue
            normalizedName = appName.strip().lower()
            if normalizedName:
                self._autoSleepAppNames.add(normalizedName)

        msg = f"SLEEP MODE MANAGER: Loaded {len(self._autoSleepAppNames)} auto-sleep apps from {self._autoSleepPath}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _isAutoSleepConfiguredForApp(self, app):
        if not app:
            return False

        if not self._autoSleepAppNames:
            return False

        appName = (AXObject.get_name(app) or "").strip().lower()
        return bool(appName and appName in self._autoSleepAppNames)

    def _setupHandlers(self):
        """Sets up and returns the sleep-mode-manager input event handlers."""

        self._handlers = {}

        self._handlers["toggleSleepMode"] = \
            input_event.InputEventHandler(
                self.toggleSleepMode,
                cmdnames.TOGGLE_SLEEP_MODE)

        msg = "SLEEP MODE MANAGER: Handlers set up."
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def _setupBindings(self):
        """Sets up and returns the sleep-mode-manager key bindings."""

        self._bindings = keybindings.KeyBindings()

        self._bindings.add(
            keybindings.KeyBinding(
                "q",
                keybindings.defaultModifierMask,
                keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
                self._handlers["toggleSleepMode"]))

        msg = "SLEEP MODE MANAGER: Bindings set up."
        debug.printMessage(debug.LEVEL_INFO, msg, True)

    def toggleSleepMode(self, script, inputEvent=None, notifyUser=True):
        """Toggles sleep mode for the active application."""

        tokens = ["SLEEP MODE MANAGER: toggleSleepMode. Script:", script,
                  "Event:", inputEvent, "notifyUser:", notifyUser]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        # Debounce rapid key presses
        currentTime = time.time()
        timeSinceLastToggle = currentTime - self._lastToggleTime
        debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Toggle attempt - time since last: {timeSinceLastToggle:.3f}s", True)
        
        if timeSinceLastToggle < self._toggleDebounceDelay:
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Ignoring rapid toggle (delay: {timeSinceLastToggle:.3f}s < {self._toggleDebounceDelay}s)", True)
            return True
        
        self._lastToggleTime = currentTime
        debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Processing toggle - app: {AXObject.get_name(script.app) if script and script.app else 'None'}", True)

        if not (script and script.app):
            return True

        self.refreshAutoSleepConfig()

        scriptManager = script_manager.get_manager()
        
        if self.isActiveForApp(script.app):
            # Turning OFF sleep mode
            appHash = hash(script.app)
            self._apps.discard(appHash)
            if self._isAutoSleepConfiguredForApp(script.app):
                self._disabledAutoSleepApps.add(appHash)
            newScript = scriptManager.get_script(script.app)
            if notifyUser:
                newScript.presentMessage(
                    messages.SLEEP_MODE_DISABLED_FOR % AXObject.get_name(script.app))
            scriptManager.set_active_script(newScript, "sleep: off")
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Disabled for {AXObject.get_name(script.app)}", True)
            # Reset debounce timer after successful toggle
            self._lastToggleTime = 0
            return True

        # Turning ON sleep mode
        braille.clear()
        if notifyUser:
            # Announce BEFORE switching to sleep mode script
            script.presentMessage(messages.SLEEP_MODE_ENABLED_FOR % AXObject.get_name(script.app))
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Scheduling delayed activation for {AXObject.get_name(script.app)}", True)
            # Use a small delay to ensure the message is spoken before switching scripts
            GLib.timeout_add(250, self._enableSleepModeDelayed, script, scriptManager)
        else:
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Immediate activation for {AXObject.get_name(script.app)}", True)
            self._enableSleepModeDelayed(script, scriptManager)
        
        return True
    
    def _enableSleepModeDelayed(self, script, scriptManager):
        """Enable sleep mode after a small delay to allow announcement."""
        
        debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: _enableSleepModeDelayed called for {AXObject.get_name(script.app) if script and script.app else 'None'}", True)
        
        try:
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Getting sleep script for {AXObject.get_name(script.app)}", True)
            sleepScript = scriptManager.get_or_create_sleep_mode_script(script.app)
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Got sleep script: {sleepScript}", True)
            
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Setting active script", True)
            scriptManager.set_active_script(sleepScript, "sleep: on")
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True)
            
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True)
            appHash = hash(script.app)
            self._disabledAutoSleepApps.discard(appHash)
            self._apps.add(appHash)
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Enabled for {AXObject.get_name(script.app)} (delayed)", True)
            # Reset debounce timer after successful toggle
            self._lastToggleTime = 0
        except Exception as e:
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error enabling delayed: {e}", True)
            import traceback
            debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Exception traceback: {traceback.format_exc()}", True)
        
        return False  # Don't repeat the timeout


_manager = SleepModeManager()

def getManager():
    """Returns the Sleep Mode Manager singleton."""
    return _manager
