#!/usr/bin/env python3
"""
MINDRACE Client — Telemetrie pilote
Lit les Shared Memory d'Assetto Corsa et envoie les donnees au serveur MINDRACE.

Installation :
    pip install requests

Configuration : modifier les 2 lignes ci-dessous, puis lancer avant de rouler.
"""

import ctypes, mmap, struct, sys, time
import requests

# ── A MODIFIER PAR CHAQUE PILOTE ──────────────────────────────────────────────
SERVER_URL  = "https://meudon-racing.ddns.net"
DRIVER_NAME = "TonPseudo"          # exactement comme dans Assetto Corsa
# ──────────────────────────────────────────────────────────────────────────────

TOKEN       = "mindrace2026"
INTERVAL    = 1.0   # secondes entre chaque envoi

ENDPOINT    = SERVER_URL.rstrip("/") + "/api/telemetry"
HEADERS     = {"X-Mindrace-Token": TOKEN, "Content-Type": "application/json"}

# ── Structures Shared Memory AC ───────────────────────────────────────────────

class Physics(ctypes.Structure):
    _fields_ = [
        ("packetId",              ctypes.c_int),
        ("gas",                   ctypes.c_float),
        ("brake",                 ctypes.c_float),
        ("fuel",                  ctypes.c_float),
        ("gear",                  ctypes.c_int),
        ("rpms",                  ctypes.c_int),
        ("steerAngle",            ctypes.c_float),
        ("speedKmh",              ctypes.c_float),
        ("velocity",              ctypes.c_float * 3),
        ("accG",                  ctypes.c_float * 3),
        ("wheelSlip",             ctypes.c_float * 4),
        ("wheelLoad",             ctypes.c_float * 4),
        ("wheelsPressure",        ctypes.c_float * 4),
        ("wheelAngularSpeed",     ctypes.c_float * 4),
        ("tyreWear",              ctypes.c_float * 4),
        ("tyreDirtyLevel",        ctypes.c_float * 4),
        ("tyreCoreTemperature",   ctypes.c_float * 4),
        ("camberRAD",             ctypes.c_float * 4),
        ("suspensionTravel",      ctypes.c_float * 4),
        ("drs",                   ctypes.c_float),
        ("tc",                    ctypes.c_float),
        ("heading",               ctypes.c_float),
        ("pitch",                 ctypes.c_float),
        ("roll",                  ctypes.c_float),
        ("cgHeight",              ctypes.c_float),
        ("carDamage",             ctypes.c_float * 5),
        ("numberOfTyresOut",      ctypes.c_int),
        ("pitLimiterOn",          ctypes.c_int),
        ("abs",                   ctypes.c_float),
        ("kersCharge",            ctypes.c_float),
        ("kersInput",             ctypes.c_float),
        ("autoShifterOn",         ctypes.c_int),
        ("rideHeight",            ctypes.c_float * 2),
        ("turboBoost",            ctypes.c_float),
        ("ballast",               ctypes.c_float),
        ("airDensity",            ctypes.c_float),
        ("airTemp",               ctypes.c_float),
        ("roadTemp",              ctypes.c_float),
        ("localAngularVel",       ctypes.c_float * 3),
        ("finalFF",               ctypes.c_float),
        ("performanceMeter",      ctypes.c_float),
        ("engineBrake",           ctypes.c_int),
        ("ersRecoveryLevel",      ctypes.c_int),
        ("ersPowerLevel",         ctypes.c_int),
        ("ersHeatCharging",       ctypes.c_int),
        ("ersIsCharging",         ctypes.c_int),
        ("kersCurrentKJ",         ctypes.c_float),
        ("drsAvailable",          ctypes.c_int),
        ("drsEnabled",            ctypes.c_int),
        ("brakeTemp",             ctypes.c_float * 4),  # FL FR RL RR
        ("clutch",                ctypes.c_float),
        ("tyreTempI",             ctypes.c_float * 4),  # interieur
        ("tyreTempM",             ctypes.c_float * 4),  # milieu
        ("tyreTempO",             ctypes.c_float * 4),  # exterieur
        ("isAIControlled",        ctypes.c_int),
        ("tyreContactPoint",      ctypes.c_float * 12),
        ("tyreContactNormal",     ctypes.c_float * 12),
        ("tyreContactHeading",    ctypes.c_float * 12),
        ("brakeBias",             ctypes.c_float),
        ("localVelocity",         ctypes.c_float * 3),
    ]

class Graphics(ctypes.Structure):
    _fields_ = [
        ("packetId",              ctypes.c_int),
        ("status",                ctypes.c_int),   # 0=off 1=replay 2=live 3=pause
        ("session",               ctypes.c_int),
        ("currentTime",           ctypes.c_wchar * 15),
        ("lastTime",              ctypes.c_wchar * 15),
        ("bestTime",              ctypes.c_wchar * 15),
        ("split",                 ctypes.c_wchar * 15),
        ("completedLaps",         ctypes.c_int),
        ("position",              ctypes.c_int),
        ("iCurrentTime",          ctypes.c_int),
        ("iLastTime",             ctypes.c_int),
        ("iBestTime",             ctypes.c_int),
        ("sessionTimeLeft",       ctypes.c_float),
        ("distanceTraveled",      ctypes.c_float),
        ("isInPit",               ctypes.c_int),
        ("currentSectorIndex",    ctypes.c_int),
        ("lastSectorTime",        ctypes.c_int),
        ("numberOfLaps",          ctypes.c_int),
        ("tyreCompound",          ctypes.c_wchar * 33),
        ("replayTimeMultiplier",  ctypes.c_float),
        ("normalizedCarPosition", ctypes.c_float),
        ("carCoordinates",        ctypes.c_float * 3),
        ("penaltyTime",           ctypes.c_float),
        ("flag",                  ctypes.c_int),
        ("idealLineOn",           ctypes.c_int),
        ("isInPitLane",           ctypes.c_int),
        ("surfaceGrip",           ctypes.c_float),
        ("mandatoryPitDone",      ctypes.c_int),
        ("windSpeed",             ctypes.c_float),
        ("windDirection",         ctypes.c_float),
    ]

class Static(ctypes.Structure):
    _fields_ = [
        ("smVersion",             ctypes.c_wchar * 15),
        ("acVersion",             ctypes.c_wchar * 15),
        ("numberOfSessions",      ctypes.c_int),
        ("numCars",               ctypes.c_int),
        ("carModel",              ctypes.c_wchar * 33),
        ("track",                 ctypes.c_wchar * 33),
        ("playerName",            ctypes.c_wchar * 33),
        ("playerSurname",         ctypes.c_wchar * 33),
        ("playerNick",            ctypes.c_wchar * 33),
        ("sectorCount",           ctypes.c_int),
        ("maxTorque",             ctypes.c_float),
        ("maxPower",              ctypes.c_float),
        ("maxRpm",                ctypes.c_int),
        ("maxFuel",               ctypes.c_float),
        ("suspensionMaxTravel",   ctypes.c_float * 4),
        ("tyreRadius",            ctypes.c_float * 4),
    ]

# ── Shared Memory ─────────────────────────────────────────────────────────────

def open_shm(name, cls):
    try:
        size = ctypes.sizeof(cls)
        mm   = mmap.mmap(-1, size, tagname=name, access=mmap.ACCESS_READ)
        return mm, cls
    except Exception:
        return None, None

def read_shm(mm, cls):
    if mm is None:
        return None
    try:
        mm.seek(0)
        raw = mm.read(ctypes.sizeof(cls))
        return cls.from_buffer_copy(raw)
    except Exception:
        return None

def list4(arr):
    return [round(arr[i], 1) for i in range(4)]

# ── Main ──────────────────────────────────────────────────────────────────────

def main():
    print("=" * 52)
    print(f"  MINDRACE Client")
    print(f"  Pilote : {DRIVER_NAME}")
    print(f"  Serveur: {SERVER_URL}")
    print("=" * 52)

    mm_phy = mm_gfx = mm_sta = None
    session = requests.Session()
    session.headers.update(HEADERS)

    consecutive_errors = 0

    while True:
        # Ouverture/reouverte des shared memory si AC demarre
        if mm_phy is None:
            mm_phy, _ = open_shm("Local\\acpmf_physics",  Physics)
            mm_gfx, _ = open_shm("Local\\acpmf_graphics", Graphics)
            mm_sta, _ = open_shm("Local\\acpmf_static",   Static)
            if mm_phy:
                print("[CLIENT] Assetto Corsa detecte — telemetrie active.")

        if mm_phy is None:
            print("[CLIENT] En attente d'Assetto Corsa...", end="\r")
            time.sleep(2)
            continue

        phy = read_shm(mm_phy, Physics)
        gfx = read_shm(mm_gfx, Graphics)
        sta = read_shm(mm_sta, Static)

        # AC pas encore en session (status 0 = AC ferme ou menu)
        if gfx is None or gfx.status == 0:
            print("[CLIENT] AC ouvert mais pas en session — attente...", end="\r")
            time.sleep(1)
            continue

        payload = {
            "name":         DRIVER_NAME,
            "speed":        round(phy.speedKmh, 1) if phy else 0,
            "gear":         (phy.gear - 1) if phy else 0,  # AC: 0=R,1=N,2=1er -> on renvoie -1=R,0=N,1=1er
            "rpms":         phy.rpms if phy else 0,
            "fuel":         round(phy.fuel, 2) if phy else 0,
            "fuel_max":     round(sta.maxFuel, 1) if sta else 0,
            "lap_start_epoch": int(time.time() * 1000) - (gfx.iCurrentTime if gfx else 0),
            "last_time":    gfx.lastTime    if gfx else "--",
            "best_time":    gfx.bestTime    if gfx else "--",
            "laps":         gfx.completedLaps if gfx else 0,
            "position":     gfx.position      if gfx else 0,
            "in_pit":       bool(gfx.isInPit or gfx.isInPitLane) if gfx else False,
            "tyre_temp_i":  list4(phy.tyreTempI)       if phy else [0,0,0,0],
            "tyre_temp_m":  list4(phy.tyreTempM)       if phy else [0,0,0,0],
            "tyre_temp_o":  list4(phy.tyreTempO)       if phy else [0,0,0,0],
            "brake_temp":   list4(phy.brakeTemp)       if phy else [0,0,0,0],
            "tyre_wear":    list4(phy.tyreWear)        if phy else [0,0,0,0],
            "tyre_compound": sta.tyresName if sta else "",
            "car":          sta.carModel   if sta else "",
            "track":        sta.track      if sta else "",
            "air_temp":     round(phy.airTemp,  1) if phy else 0,
            "road_temp":    round(phy.roadTemp, 1) if phy else 0,
            "throttle":     round(phy.gas   * 100) if phy else 0,
            "brake_pct":    round(phy.brake * 100) if phy else 0,
        }

        try:
            r = session.post(ENDPOINT, json=payload, timeout=3)
            if r.status_code == 200:
                consecutive_errors = 0
                print(f"[CLIENT] {DRIVER_NAME}  {payload['speed']:.0f}km/h  G{payload['gear']}  "
                      f"Lap {payload['laps']}  {payload['current_time']}  "
                      f"Fuel {payload['fuel']:.1f}L     ", end="\r")
            elif r.status_code == 401:
                print("\n[CLIENT] ERREUR : token invalide. Verifiez DRIVER_NAME et TOKEN.")
                sys.exit(1)
            else:
                consecutive_errors += 1
                print(f"\n[CLIENT] Serveur: HTTP {r.status_code} ({consecutive_errors} erreurs)")
        except requests.exceptions.ConnectionError:
            consecutive_errors += 1
            if consecutive_errors == 1 or consecutive_errors % 10 == 0:
                print(f"\n[CLIENT] Impossible de joindre {SERVER_URL} ({consecutive_errors} tentatives)")
        except Exception as e:
            consecutive_errors += 1
            print(f"\n[CLIENT] Erreur: {e}")

        time.sleep(INTERVAL)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n[CLIENT] Arrete.")
