import ctypes
import ctypes.wintypes
import sys
import time
import threading


if not hasattr(ctypes, "windll"):
    print("Скрипт работает только под Windows.")
    sys.exit(1)


# ==========================================================
# DDC/CI
# ==========================================================

DDC_POWER_VCP = 0xD6
DDC_POWER_ON = 1
DDC_POWER_OFF = 5


# ==========================================================
# Windows constants
# ==========================================================

ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001

WH_KEYBOARD_LL = 13
PM_REMOVE = 0x0001
VK_ESCAPE = 0x1B

PHYSICAL_MONITOR_DESCRIPTION_SIZE = 128

# Console control events
CTRL_C_EVENT = 0
CTRL_BREAK_EVENT = 1
CTRL_CLOSE_EVENT = 2
CTRL_LOGOFF_EVENT = 5
CTRL_SHUTDOWN_EVENT = 6


STOP_EVENT = threading.Event()
MONITOR_RESTORED = threading.Event()

_DDC_LOCK = threading.Lock()
_RESTORE_LOCK = threading.Lock()


user32 = ctypes.WinDLL("user32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
dxva2 = ctypes.WinDLL("dxva2", use_last_error=True)


# ==========================================================
# Structures
# ==========================================================

class PHYSICAL_MONITOR(ctypes.Structure):
    _fields_ = [
        ("hPhysicalMonitor", ctypes.c_void_p),
        ("szPhysicalMonitorDescription", ctypes.c_wchar * PHYSICAL_MONITOR_DESCRIPTION_SIZE),
    ]


class KBDLLHOOKSTRUCT(ctypes.Structure):
    _fields_ = [
        ("vkCode", ctypes.wintypes.DWORD),
        ("scanCode", ctypes.wintypes.DWORD),
        ("flags", ctypes.wintypes.DWORD),
        ("time", ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.c_void_p),
    ]


HOOKPROC = ctypes.WINFUNCTYPE(
    ctypes.c_long,
    ctypes.c_int,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM
)

MONITOR_ENUM_PROC = ctypes.WINFUNCTYPE(
    ctypes.wintypes.BOOL,
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.wintypes.LPARAM
)

HANDLER_ROUTINE = ctypes.WINFUNCTYPE(
    ctypes.wintypes.BOOL,
    ctypes.wintypes.DWORD
)


# ==========================================================
# API signatures
# ==========================================================

kernel32.GetModuleHandleW.argtypes = [ctypes.wintypes.LPCWSTR]
kernel32.GetModuleHandleW.restype = ctypes.c_void_p

kernel32.SetThreadExecutionState.argtypes = [ctypes.wintypes.DWORD]
kernel32.SetThreadExecutionState.restype = ctypes.wintypes.DWORD

kernel32.SetConsoleCtrlHandler.argtypes = [HANDLER_ROUTINE, ctypes.wintypes.BOOL]
kernel32.SetConsoleCtrlHandler.restype = ctypes.wintypes.BOOL


user32.SetWindowsHookExW.argtypes = [
    ctypes.c_int,
    HOOKPROC,
    ctypes.c_void_p,
    ctypes.wintypes.DWORD
]
user32.SetWindowsHookExW.restype = ctypes.c_void_p

user32.UnhookWindowsHookEx.argtypes = [ctypes.c_void_p]
user32.UnhookWindowsHookEx.restype = ctypes.wintypes.BOOL

user32.CallNextHookEx.argtypes = [
    ctypes.c_void_p,
    ctypes.c_int,
    ctypes.wintypes.WPARAM,
    ctypes.wintypes.LPARAM
]
user32.CallNextHookEx.restype = ctypes.c_long

user32.EnumDisplayMonitors.argtypes = [
    ctypes.c_void_p,
    ctypes.c_void_p,
    MONITOR_ENUM_PROC,
    ctypes.wintypes.LPARAM
]
user32.EnumDisplayMonitors.restype = ctypes.wintypes.BOOL

user32.PeekMessageW.argtypes = [
    ctypes.POINTER(ctypes.wintypes.MSG),
    ctypes.c_void_p,
    ctypes.wintypes.UINT,
    ctypes.wintypes.UINT,
    ctypes.wintypes.UINT
]
user32.PeekMessageW.restype = ctypes.wintypes.BOOL

user32.TranslateMessage.argtypes = [ctypes.POINTER(ctypes.wintypes.MSG)]
user32.TranslateMessage.restype = ctypes.wintypes.BOOL

user32.DispatchMessageW.argtypes = [ctypes.POINTER(ctypes.wintypes.MSG)]
user32.DispatchMessageW.restype = ctypes.c_long


dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR.argtypes = [
    ctypes.c_void_p,
    ctypes.POINTER(ctypes.wintypes.DWORD)
]
dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR.restype = ctypes.wintypes.BOOL

dxva2.GetPhysicalMonitorsFromHMONITOR.argtypes = [
    ctypes.c_void_p,
    ctypes.wintypes.DWORD,
    ctypes.POINTER(PHYSICAL_MONITOR)
]
dxva2.GetPhysicalMonitorsFromHMONITOR.restype = ctypes.wintypes.BOOL

dxva2.SetVCPFeature.argtypes = [
    ctypes.c_void_p,
    ctypes.c_ubyte,
    ctypes.wintypes.DWORD
]
dxva2.SetVCPFeature.restype = ctypes.wintypes.BOOL

dxva2.DestroyPhysicalMonitors.argtypes = [
    ctypes.wintypes.DWORD,
    ctypes.POINTER(PHYSICAL_MONITOR)
]
dxva2.DestroyPhysicalMonitors.restype = ctypes.wintypes.BOOL


# ==========================================================
# DDC/CI monitor handling
# ==========================================================

_physical_arrays = []


def enum_hmonitors():
    """
    Возвращает список HMONITOR.
    """
    hmonitors = []

    def monitor_enum_proc(hmon, hdc, lprc, lparam):
        hmonitors.append(hmon)
        return 1

    proc = MONITOR_ENUM_PROC(monitor_enum_proc)
    user32.EnumDisplayMonitors(None, None, proc, 0)

    return hmonitors


def init_physical_monitors_unlocked():
    """
    Получает физические мониторы.
    Вызывать только внутри _DDC_LOCK.
    """
    global _physical_arrays

    if _physical_arrays:
        return

    for hmon in enum_hmonitors():
        count = ctypes.wintypes.DWORD(0)

        if not dxva2.GetNumberOfPhysicalMonitorsFromHMONITOR(hmon, ctypes.byref(count)):
            continue

        if count.value <= 0:
            continue

        arr_type = PHYSICAL_MONITOR * count.value
        arr = arr_type()

        if not dxva2.GetPhysicalMonitorsFromHMONITOR(hmon, count.value, arr):
            continue

        _physical_arrays.append((count.value, arr))


def ddc_power(value: int) -> bool:
    """
    Устанавливает VCP 0xD6 для всех найденных физических мониторов.
    """
    acquired = _DDC_LOCK.acquire(timeout=1.0)
    if not acquired:
        return False

    try:
        if not _physical_arrays:
            init_physical_monitors_unlocked()

        if not _physical_arrays:
            return False

        ok = False

        for count, arr in _physical_arrays:
            for i in range(count):
                if dxva2.SetVCPFeature(arr[i].hPhysicalMonitor, DDC_POWER_VCP, value):
                    ok = True

        return ok

    finally:
        _DDC_LOCK.release()


def close_physical_monitors():
    """
    Освобождает физические мониторы.
    """
    acquired = _DDC_LOCK.acquire(timeout=1.0)
    if not acquired:
        return

    try:
        for count, arr in _physical_arrays:
            dxva2.DestroyPhysicalMonitors(count, arr)

        _physical_arrays.clear()

    finally:
        _DDC_LOCK.release()


def restore_monitor_once() -> bool:
    """
    Включает монитор один раз.
    Используется в Esc, finally, закрытии консоли и завершении работы.
    """
    with _RESTORE_LOCK:
        if MONITOR_RESTORED.is_set():
            return True

        try:
            ok = ddc_power(DDC_POWER_ON)
        except Exception:
            ok = False

        if ok:
            MONITOR_RESTORED.set()

        return ok


# ==========================================================
# Console control handler
# ==========================================================

_console_ctrl_handler_ref = None


def console_ctrl_handler(ctrl_type):
    """
    Обработчик системных событий консоли:
    закрытие окна консоли, выход из системы, выключение/перезагрузка.
    """
    try:
        if ctrl_type in (
            CTRL_CLOSE_EVENT,
            CTRL_LOGOFF_EVENT,
            CTRL_SHUTDOWN_EVENT
        ):
            restore_monitor_once()
            return True

    except Exception:
        pass

    return False


def install_console_ctrl_handler():
    """
    Устанавливает обработчик закрытия консоли и завершения сеанса.
    """
    global _console_ctrl_handler_ref

    _console_ctrl_handler_ref = HANDLER_ROUTINE(console_ctrl_handler)
    kernel32.SetConsoleCtrlHandler(_console_ctrl_handler_ref, True)


# ==========================================================
# Keyboard hook
# ==========================================================

keyboard_hook = None


def keyboard_callback(nCode, wParam, lParam):
    """
    Низкоуровневый хук клавиатуры.

    Обычные клавиши работают.
    Esc включает монитор и завершает скрипт.
    """
    if nCode >= 0:
        try:
            kb = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents

            if kb.vkCode == VK_ESCAPE:
                STOP_EVENT.set()
                return 1

        except Exception:
            pass

    return user32.CallNextHookEx(keyboard_hook, nCode, wParam, lParam)


keyboard_proc = HOOKPROC(keyboard_callback)


def install_keyboard_hook():
    global keyboard_hook

    h_mod = kernel32.GetModuleHandleW(None)

    keyboard_hook = user32.SetWindowsHookExW(
        WH_KEYBOARD_LL,
        keyboard_proc,
        h_mod,
        0
    )

    if not keyboard_hook:
        raise OSError("Не удалось установить хук клавиатуры.")


def message_loop():
    """
    Цикл сообщений нужен для работы low-level хука.
    """
    msg = ctypes.wintypes.MSG()

    while not STOP_EVENT.is_set():
        ret = user32.PeekMessageW(
            ctypes.byref(msg),
            None,
            0,
            0,
            PM_REMOVE
        )

        if ret > 0:
            user32.TranslateMessage(ctypes.byref(msg))
            user32.DispatchMessageW(ctypes.byref(msg))
        else:
            time.sleep(0.01)


# ==========================================================
# Power state
# ==========================================================

def prevent_system_sleep():
    kernel32.SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)


def restore_power_settings():
    kernel32.SetThreadExecutionState(ES_CONTINUOUS)


# ==========================================================
# Main
# ==========================================================

def main():
    print("Запуск: монитор будет выключен через DDC/CI.")
    print("Включение и выход — клавишей Esc.")
    print("Остальные клавиши и мышь работают штатно.")

    prevent_system_sleep()

    # Расширенная защита на случай закрытия консоли,
    # выхода из системы и выключения компьютера.
    try:
        install_console_ctrl_handler()
    except Exception:
        pass

    try:
        if not ddc_power(DDC_POWER_OFF):
            print("DDC/CI не ответил. Проверьте монитор, кабель и поддержку DDC/CI.")
            return

        install_keyboard_hook()
        message_loop()

    except KeyboardInterrupt:
        print("\nОстановка по Ctrl+C...")

    except Exception as exc:
        print(f"Ошибка: {exc}")

    finally:
        STOP_EVENT.set()

        if keyboard_hook:
            try:
                user32.UnhookWindowsHookEx(keyboard_hook)
            except Exception:
                pass

        # Включаем монитор при любом штатном выходе.
        restore_monitor_once()

        restore_power_settings()
        close_physical_monitors()

        if MONITOR_RESTORED.is_set():
            print("Скрипт завершен, монитор включен.")
        else:
            print("Скрипт завершен, но монитор, возможно, остался выключенным.")


if __name__ == "__main__":
    main()