"""Main pythoncom loop for Winston application.

This module implements the keyboard event handler and main message loop for the
Winston application. It is launched by the top-level script (churchill.py) and
can be restarted on crashes.

Key responsibilities:
- Instantiate Winston application (Anchor)
- Install keyboard hook using pyWinhook
- Run pythoncom message pump loop
- Route keyboard events to active applet
- Handle cleanup and graceful shutdown
- Support restart capability

The module uses pyWinhook to intercept keyboard events globally, filters events
to only process those targeting Winston windows, and routes keys through the
ModifiedKey system to the active applet's key queue.
"""

from __future__ import annotations
from typing import Any, TYPE_CHECKING
import pythoncom  # type: ignore[import]
import win32process  # type: ignore[import]
import win32gui  # type: ignore[import]
import pygetwindow as gw  # type: ignore[import]
import pyautogui  # type: ignore[import]
import win32api  # type: ignore[import]
import pyWinhook as pyHook  # type: ignore[import]
import sys
import os
from datetime import datetime as dt
import time
import traceback

if TYPE_CHECKING:
    from .framework import Anchor

from . import framework

framework.beep(200, 300)

# Set the directory containing this script as the working directory
# (assumes that framework and other winstonian files are all in the same directory
# as they liberally use basenames to access winstonian config and output files)
dirname = os.path.dirname(__file__)
os.chdir(dirname)

# Add the snippets and scripts subdirectories to the import path
sys.path.extend([os.path.join(dirname, 'snippets'), os.path.join(dirname, 'scripts')])

# Global app instance used by event handlers
app: Anchor


def event_for_us(app: Anchor, event: Any) -> bool:
    """Check if keyboard event targets Winston window or helper window.

    F11 toggles key filtering. When filtering is disabled, all keystrokes are
    printed along with whether they would have been accepted if filtering were active.

    Args:
        app: The Anchor application instance.
        event: pyWinhook keyboard event object.

    Returns:
        True if the event should be processed by Winston, False otherwise.
    """
    if event.Key == 'F11':
        app.toggle_key_filter()
    if app.key_filter:
        return event.Window in app.window_handles
    else:
        aw = gw.getActiveWindow()
        # print(event.Key, event.Window, event.WindowName, aw._hWnd, aw.title)
        return True


def reset_modifiers(app: Anchor) -> None:
    """Release all modifier keys and clear Winston's modifier state.

    Called when Winston regains focus to ensure clean modifier state.

    Args:
        app: The Anchor application instance.
    """
    # List of modifier keys to reset
    modifier_keys = ['alt', 'ctrl', 'shift', 'win']
    for key in modifier_keys:
        pyautogui.keyUp(key)
    app.modifiers.clear()  # Also clear Winston's view of the modifier states
    # print(dt.now().strftime('Reset modifier keys at %H:%M:%S'))


def add_hook(app: Anchor) -> None:
    """Install keyboard hook to intercept keyboard events.

    Creates HookManager instance if not present and registers KeyDown/KeyUp handlers.

    Args:
        app: The Anchor application instance.
    """
    # print('in add hook, should try force del and re-add of hook')
    if not hasattr(app, 'hm'):
        app.hm = pyHook.HookManager()  # type: ignore[attr-defined]
        app.hm.KeyDown = On_key_down  # type: ignore[attr-defined]
        app.hm.KeyUp = On_key_up  # type: ignore[attr-defined]
        # print(dt.now().strftime('hooking keyboard %H:%M:%S'))
        app.hm.HookKeyboard()  # type: ignore[attr-defined]


def remove_hook(app: Anchor) -> None:
    """Remove keyboard hook.

    Unhooks keyboard and deletes HookManager instance.

    Args:
        app: The Anchor application instance.
    """
    if hasattr(app, 'hm') and hasattr(app.hm, 'keyboard_hook') and app.hm.keyboard_hook:
        # print(dt.now().strftime('unhooking keyboard %H:%M:%S'))
        app.hm.UnhookKeyboard()
        del app.hm


def forward_key(event: Any) -> bool:
    """Determine which keys pass through to system vs handled by Winston.

    Global system keys like volume controls and media keys always pass through.
    Left Windows key combinations pass through for system shortcuts.
    Alt+Tab, Alt+F4, Alt+Control pass through.
    Everything else is blocked and handled by Winston.

    Args:
        event: pyWinhook keyboard event object.

    Returns:
        True to pass key to system, False to handle in Winston.
    """
    key = event.Key

    # Always pass through modifier keys alone
    if key in ['Lshift', 'Rshift', 'Lmenu', 'Rcontrol']:
        return True

    # If left windows key is down, pass everything through and remove hook
    # (allows system shortcuts like Win+L, Win+D, etc.)
    if 'Lwin' in app.modifiers:
        remove_hook(app)
        return True

    # Always pass through global system keys that users expect to work everywhere
    global_system_keys = [
        # Volume controls
        'Volume_Down', 'Volume_Up', 'Volume_Mute',
        # Media controls
        'Media_Play_Pause', 'Media_Stop', 'Media_Next_Track', 'Media_Prev_Track',
        # System keys
        'Print_Screen', 'Scroll_Lock', 'Pause',
        # Power management
        'Sleep', 'Power',
        # Browser/application controls that should work globally
        'Browser_Back', 'Browser_Forward', 'Browser_Refresh', 'Browser_Search',
        'Browser_Favorites', 'Browser_Home', 'Browser_Stop',
        'Launch_App1', 'Launch_App2', 'Launch_Mail', 'Launch_Media_Select'
    ]
    if key in global_system_keys:
        return True

    # Pass through Alt+Tab, Alt+F4, and Alt+Control combinations
    if 'Lmenu' in app.modifiers:
        if key in ['Tab', 'F4', 'Lcontrol']:
            return True

    # Block everything else - these will be handled by winston
    return False


def On_key_up(event: Any) -> bool:
    """Handle key up events.

    Removes key from modifier set. If app is quitting and all modifiers released,
    posts quit message to exit message loop.

    Args:
        event: pyWinhook keyboard event object.

    Returns:
        Always True to pass event to other handlers.
    """
    app.modifiers.discard(event.Key)  # Remove this key if in modifier list
    # if event.Key == 'Capital':
    #     print('releasing caps lock')
    if app.quitting:
        if not app.modifiers:
            win32api.PostQuitMessage(0)  # Exit after allowing all modifiers to be unpressed
    return True  # Pass event on to other handlers


def On_key_down(event: Any) -> bool:
    """Handle key down events.

    Main keyboard event handler. Filters events, updates modifier state, and routes
    keys to active applet's key queue via ModifiedKey objects.

    Special keys:
    - Control+Escape: Quit without restart
    - Control+Alt+critical_keys: Execute critical key handlers
    - CapsLock: Suppressed to prevent conflicts with JAWS

    Args:
        event: pyWinhook keyboard event object.

    Returns:
        True to pass key to system, False if handled by Winston.
    """
    rc = True
    try:
        # Cancel any pending phonetic speak of last char as we now have the next key press
        if hasattr(app, "pending_phonetic"):
            if app.pending_phonetic.is_alive():
                app.pending_phonetic.cancel()

        # Ignore all keys not destined for this app
        if event_for_us(app, event):
            # Discard injected or fake events
            if event.Injected or (event.Key is None) or (event.Key.lower() in ('noname',)):
                print('discarding injected or fake', event)
                rc = True
            elif ('Lcontrol' in app.modifiers) and event.Key == 'Escape':
                app.quitting = True
                app.restartable = False
                rc = False
            elif (set(['Lmenu', 'Lcontrol']).issubset(app.modifiers)) and (event.Key in app.critical_keys.handlers):
                app.critical_keys.handlers.get(event.Key, framework.beep)()
                rc = False
            elif event.Key == 'Capital':  # Suppress Caps Lock
                rc = False
            else:  # Process normal keys
                if event.Key in framework.modifier_keys:
                    # Update modifier state but still pass on some modifiers
                    app.modifiers.add(event.Key)
                rc = forward_key(event)  # Decides what to pass on to Windows
                # print(f'return from Forward is {rc} for {event.Key}')
                if not rc:  # Handle keys not filtered out by forward_key
                    # Remove this key from modifiers to avoid names like Control+Lcontrol
                    mods = app.modifiers - set([event.Key])
                    ascii = None if not hasattr(event, 'Ascii') else event.Ascii
                    k = framework.ModifiedKey(event.Key, mods, ascii, event)  # type: ignore[arg-type]
                    # print(f'putting {k.modified_name} on q for current applet')
                    applet = app.applets.current()
                    if applet is not None:
                        applet.key_q.put(k)
                    else:
                        print(f'applets.current() is None, with len(app.applets): {len(app.applets)} and cursor {app.applets.cursor} so exiting key hook with no action')
                        sys.stdout.flush()
                        win32api.PostQuitMessage(0)
    except KeyboardInterrupt:
        # Exempt Control+C from quitting the app
        rc = False
    except Exception as e:
        print(f'{e} caught in on_key_down')
        traceback.print_exc()
        sys.stdout.flush()
        win32api.PostQuitMessage(0)
        rc = True  # Just in case exception occurred handling Alt+Tab
    finally:
        # print('finally with key ', event.Key, rc)
        if rc not in [True, False]:
            framework.beep(1500, 1000, tag=f'{event.Key} hook exit rc: {rc}')
        sys.stdout.flush()
    return rc


def main(restartable: bool) -> bool:
    """Main pythoncom message loop.

    Creates Anchor application instance, installs keyboard hook, runs message pump,
    monitors window focus changes, and handles graceful shutdown.

    Args:
        restartable: If True, allow restart on crash; if False, exit permanently.

    Returns:
        True if app should restart, False if app should exit permanently.
    """
    global app
    app = framework.Anchor(restartable)
    add_hook(app)
    last_active_window = None
    looping_time = 0
    sleep_time = 0.05
    pyautogui.FAILSAFE = False
    msg = None

    while not (app.quit_now or pythoncom.PumpWaitingMessages()):
        if looping_time >= 0.5:
            current_active_window = gw.getActiveWindow()
            if current_active_window != last_active_window:
                hwnd = getattr(current_active_window, '_hWnd', 'handleless window')
                tit = getattr(current_active_window, 'title', 'Untitled window')
                # print('Window focus has changed to:', tit)
                last_active_window = current_active_window
                # print(app.window_titles, tit in app.window_titles)
                # print(app.window_handles, hwnd in app.window_handles)
                if hwnd in app.window_handles:
                    reset_modifiers(app)  # Winston regained focus
                    try:
                        # app.tts.speak('', interrupt=True)
                        # print('winston regained focus')
                        pass
                    except Exception as e:
                        print(e)
                    add_hook(app)
                    sleep_time = 0.05
                else:
                    remove_hook(app)
                    sleep_time = 0.5
        # If using SAPI then need to dequeue and handle pending speech here,
        # but latency is too bad to make this useable.
        # app.sapi_tts.process_functions()
        time.sleep(sleep_time)
        looping_time += sleep_time  # type: ignore[assignment]

    # Set app.quit_now in case message pump exited because of PostQuitMessage
    app.quit_now = True
    restart_app = app.restartable
    stuck_threads = []

    # Clear down any debugger session
    if getattr(app, 'debugger', None) and app.debugger.startup_complete:
        if app.debugger.applet:
            try:
                app.debugger.set_quit()
            except Exception as e:
                print(f'caught {e} in closedown processing in clem')
                pass

    # Copy app.applets before starting to send signals to avoid multi-threading glitches
    app_list = [applet for applet in app.applets]
    for applet in app_list:
        applet.key_q.put('quit_now')
        applet.thread.join(0.5)  # type: ignore[attr-defined] # Wait 0.5 seconds for each applet task to end
        if applet.thread.is_alive():  # type: ignore[attr-defined]
            stuck_threads.append(applet)

    if len(stuck_threads):
        print(f'\nterminating with stuck thread(s): {[a.name for a in stuck_threads]}')

    app.tracer.close()  # Close call_trace.log
    orator = app.window_titles[0]
    app.tts.speak('restarting' if app.restartable else f'Goodbye from {orator}')

    # Flushing sys.stdout ensures all output gets written to output.txt before winston departs or restarts
    sys.stdout.flush()  # type: ignore[union-attr]
    sys.stderr.flush()  # type: ignore[union-attr]
    sys.stdout = sys.__stdout__
    sys.stderr = sys.__stderr__
    remove_hook(app)
    sys.stdout.flush()  # type: ignore[union-attr]
    sys.stderr.flush()  # type: ignore[union-attr]
    return restart_app


if __name__ == '__main__':
    main(restartable=False)