import base64
import hashlib
import json
import time
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]

import websocket


SOURCE_NAME = 'Tibia'
SCENE_NAME = 'TibiaPilotNG'


def receive(ws, expected_op):
    while True:
        message = json.loads(ws.recv())
        if message.get('op') == expected_op:
            return message['d']


def request(ws, request_type, request_data=None):
    ws.send(json.dumps({
        'op': 6,
        'd': {
            'requestType': request_type,
            'requestId': request_type,
            'requestData': request_data or {},
        },
    }))
    response = receive(ws, 7)
    status = response['requestStatus']
    if not status['result']:
        raise RuntimeError(f"{request_type}: {status.get('comment', 'failed')}")
    return response.get('responseData', {})


def authenticate():
    config_path = Path.home() / (
        'AppData/Roaming/obs-studio/plugin_config/obs-websocket/config.json'
    )
    config = json.loads(config_path.read_text(encoding='utf-8-sig'))
    ws = websocket.create_connection(
        f"ws://127.0.0.1:{config.get('server_port', 4455)}", timeout=5,
    )
    hello = receive(ws, 0)
    identify = {'rpcVersion': hello['rpcVersion']}
    authentication = hello.get('authentication')
    if authentication:
        secret = base64.b64encode(hashlib.sha256(
            (config['server_password'] + authentication['salt']).encode()
        ).digest()).decode()
        identify['authentication'] = base64.b64encode(hashlib.sha256(
            (secret + authentication['challenge']).encode()
        ).digest()).decode()
    ws.send(json.dumps({'op': 1, 'd': identify}))
    receive(ws, 2)
    return ws


def selected_game_window(ws, game_title):
    items = request(ws, 'GetInputPropertiesListPropertyItems', {
        'inputName': SOURCE_NAME,
        'propertyName': 'window',
    })['propertyItems']
    candidates = [item for item in items if item['itemValue'].endswith(':client.exe')]
    exact = [
        item for item in candidates
        if item['itemValue'].startswith(f'{game_title}:')
    ]
    titled = [
        item for item in candidates
        if item['itemName'].lower().startswith('[client.exe]: tibia - ')
    ]
    if len(exact) == 1:
        return exact[0]['itemValue']
    if len(titled) == 1:
        return titled[0]['itemValue']
    raise RuntimeError('OBS konnte das gestartete Tibia-Fenster nicht eindeutig finden.')


def projector_monitor(ws, game):
    """Bildschirm für den Projektor: der virtuelle, NICHT der des Spiels.

    Der Bot liest sein Bild von dem Bildschirm, auf dem der Projektor
    läuft. Läge der Projektor auf demselben Bildschirm wie der Client,
    gäbe es zwei Probleme: Der randlose Vollbild-Client blockiert die
    Bildschirmaufnahme (Windows liefert dann nur Schwarz), und jeder
    Klick ins Spiel holt den Client nach vorne und verdeckt den
    Projektor. Beides gemessen. Darum bekommt der Projektor den
    virtuellen Bildschirm für sich allein.
    """
    left, top, right, bottom = game['monitor']
    spiel = (left, top, right - left, bottom - top)
    if spiel[2:] != (1920, 1080):
        raise RuntimeError('Der Bot-Bildschirm muss 1920×1080 haben.')
    monitore = request(ws, 'GetMonitorList')['monitors']

    def lage(item):
        return (
            item['monitorPositionX'], item['monitorPositionY'],
            item['monitorWidth'], item['monitorHeight'],
        )

    frei = [
        item for item in monitore
        if item['monitorWidth'] == 1920 and item['monitorHeight'] == 1080
        and lage(item) != spiel
    ]
    if not frei:
        raise RuntimeError(
            'Für den Projektor fehlt ein zweiter 1920×1080-Bildschirm. '
            'Der virtuelle Bildschirm ist vermutlich nicht aktiv.'
        )
    # Der virtuelle Bildschirm liegt rechts von allen echten.
    return max(frei, key=lambda item: item['monitorPositionX'])


def close_existing_projectors():
    """Alle offenen Projektor-Fenster schließen.

    OBS' WebSocket-Schnittstelle kann Projektoren öffnen, aber nicht mehr
    schließen (CloseProjector gibt es in Version 5 nicht mehr). Ohne
    dieses Aufräumen sammelt sich bei jedem Startversuch ein weiteres
    Fenster an - nach zwei Läufen standen vier davon offen.
    """
    import win32con
    import win32gui

    offen = []

    def sammeln(hwnd, _):
        if not win32gui.IsWindowVisible(hwnd):
            return
        titel = win32gui.GetWindowText(hwnd)
        # Deutsch "Projektor - ...", englisch "Fullscreen Projector ..."
        if 'Projektor' in titel or 'Projector' in titel:
            offen.append(hwnd)

    win32gui.EnumWindows(sammeln, None)
    for hwnd in offen:
        win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0)
    return len(offen)


def main():
    game = json.loads((REPO / '.play-tibia.json').read_text(encoding='utf-8'))
    # Erst aufraeumen, dann genau EINEN neuen Projektor oeffnen.
    geschlossen = close_existing_projectors()
    if geschlossen:
        time.sleep(0.6)
    ws = authenticate()
    try:
        window = selected_game_window(ws, game['game'])
        monitor = projector_monitor(ws, game)
        request(ws, 'SetInputSettings', {
            'inputName': SOURCE_NAME,
            'inputSettings': {
                'window': window,
                'capture_mode': 'window',
                'capture_cursor': False,
                'priority': 0,
            },
            'overlay': False,
        })
        request(ws, 'SetCurrentProgramScene', {'sceneName': SCENE_NAME})
        request(ws, 'OpenSourceProjector', {
            'sourceName': SOURCE_NAME,
            'monitorIndex': monitor['monitorIndex'],
        })
        active = request(ws, 'GetSourceActive', {'sourceName': SOURCE_NAME})
    finally:
        ws.close()

    (REPO / '.obs-projector.json').write_text(json.dumps({
        'source': SOURCE_NAME,
        'window': window,
        'scene': SCENE_NAME,
        'monitor': monitor,
        'closed': geschlossen,
        'projectors': 1,
        'active': active,
    }), encoding='utf-8')


if __name__ == '__main__':
    main()
