import json
import os
from pathlib import Path

import win32api
import win32con


LOCAL_ROOT = Path(os.getenv(
    'LOCALAPPDATA', Path.home() / 'AppData' / 'Local',
)) / 'TibiaPilotNG'
SETTINGS_PATH = LOCAL_ROOT / 'display.json'
BACKUP_PATH = LOCAL_ROOT / 'display-backup.json'


def getDisplays():
    displays = []
    for deviceIndex in range(32):
        try:
            adapter = win32api.EnumDisplayDevices(None, deviceIndex)
        except Exception:
            break
        if not adapter.StateFlags & 1:
            continue
        try:
            mode = _currentMode(adapter.DeviceName)
        except Exception:
            continue
        outputIndex = len(displays)
        name = adapter.DeviceString or 'Monitor'
        virtual = any(word in name.lower() for word in ('virtual', 'usb mobile'))
        number = outputIndex + 1
        label = f'Bildschirm {number} — {mode.PelsWidth}×{mode.PelsHeight}'
        if adapter.StateFlags & 4:
            label += ' — Primär'
        if virtual:
            label += ' — Virtuell'
        displays.append({
            'device': adapter.DeviceName,
            'name': name,
            'number': number,
            'label': label,
            'x': int(mode.Position_x),
            'y': int(mode.Position_y),
            'width': int(mode.PelsWidth),
            'height': int(mode.PelsHeight),
            'primary': bool(adapter.StateFlags & 4),
            'virtual': virtual,
            'outputIndex': outputIndex,
        })
    return displays


def getSelectedDisplay():
    displays = getDisplays()
    if not displays:
        raise RuntimeError('Windows hat keinen Bildschirm erkannt.')
    selectedDevice = None
    try:
        selectedDevice = json.loads(
            SETTINGS_PATH.read_text(encoding='utf-8-sig'),
        ).get('device')
    except (OSError, ValueError):
        pass
    selected = next(
        (display for display in displays if display['device'] == selectedDevice),
        None,
    )
    if selected is not None:
        return selected
    return next(
        (display for display in displays if display['virtual']),
        next((display for display in displays if display['primary']), displays[0]),
    )


def getCaptureDisplay():
    """Der Bildschirm, von dem der Bot sein Bild liest.

    Das ist NICHT der Bildschirm des Spiels, sondern der virtuelle, auf
    dem der OBS-Projektor laeuft. Grund: Der Client laeuft randlos im
    Vollbild, und Windows liefert von einem solchen Bildschirm nur
    Schwarz an die Bildschirmaufnahme. Der Projektor ist dagegen ein
    normales Fenster und damit lesbar. Ausserdem wuerde jeder Klick ins
    Spiel den Client nach vorne holen und einen Projektor auf demselben
    Bildschirm verdecken.

    Faellt auf den ausgewaehlten Bildschirm zurueck, wenn es keinen
    virtuellen gibt - dann kann der Bot wenigstens etwas sehen.
    """
    displays = getDisplays()
    if not displays:
        raise RuntimeError('Windows hat keinen Bildschirm erkannt.')
    spiel = getSelectedDisplay()
    frei = [
        display for display in displays
        if (display['width'], display['height']) == (1920, 1080)
        and display['device'] != spiel['device']
    ]
    if not frei:
        return spiel
    virtuell = [display for display in frei if display['virtual']]
    kandidaten = virtuell or frei
    return max(kandidaten, key=lambda display: display['x'])


def saveSelectedDisplay(device):
    display = next(
        (item for item in getDisplays() if item['device'] == device), None,
    )
    if display is None:
        raise ValueError('Der ausgewählte Bildschirm ist nicht verfügbar.')
    LOCAL_ROOT.mkdir(parents=True, exist_ok=True)
    temporary = SETTINGS_PATH.with_suffix('.tmp')
    temporary.write_text(
        json.dumps({'device': device}, ensure_ascii=False), encoding='utf-8',
    )
    os.replace(temporary, SETTINGS_PATH)
    return display


def _currentMode(device):
    return win32api.EnumDisplaySettings(device, win32con.ENUM_CURRENT_SETTINGS)


def _modeData(mode):
    return {
        'width': int(mode.PelsWidth),
        'height': int(mode.PelsHeight),
        'bits': int(mode.BitsPerPel),
        'frequency': int(mode.DisplayFrequency),
    }


def _applyMode(device, settings):
    mode = _currentMode(device)
    mode.PelsWidth = int(settings['width'])
    mode.PelsHeight = int(settings['height'])
    mode.BitsPerPel = int(settings.get('bits', mode.BitsPerPel))
    mode.DisplayFrequency = int(settings.get('frequency', mode.DisplayFrequency))
    tested = win32api.ChangeDisplaySettingsEx(device, mode, win32con.CDS_TEST)
    if tested != win32con.DISP_CHANGE_SUCCESSFUL:
        raise RuntimeError('1920×1080 wird auf diesem Bildschirm nicht unterstützt.')
    applied = win32api.ChangeDisplaySettingsEx(
        device, mode, win32con.CDS_UPDATEREGISTRY)
    if applied != win32con.DISP_CHANGE_SUCCESSFUL:
        raise RuntimeError('Die Bildschirmauflösung konnte nicht geändert werden.')


def prepareSelectedDisplay():
    selected = getSelectedDisplay()
    mode = _currentMode(selected['device'])
    current = _modeData(mode)
    if current['width'] != 1920 or current['height'] != 1080:
        LOCAL_ROOT.mkdir(parents=True, exist_ok=True)
        if not BACKUP_PATH.exists():
            BACKUP_PATH.write_text(json.dumps({
                'device': selected['device'], **current,
            }), encoding='utf-8')
        _applyMode(selected['device'], {
            **current, 'width': 1920, 'height': 1080,
        })
        selected = getSelectedDisplay()
    if selected['width'] != 1920 or selected['height'] != 1080:
        raise RuntimeError('Der Bot-Bildschirm ist nicht auf 1920×1080.')
    return selected


def restoreDisplay():
    if not BACKUP_PATH.exists():
        return False
    backup = json.loads(BACKUP_PATH.read_text(encoding='utf-8-sig'))
    _applyMode(backup.pop('device'), backup)
    BACKUP_PATH.unlink(missing_ok=True)
    return True
