from __future__ import annotations

import ctypes
import time
from ctypes import wintypes
from dataclasses import dataclass
from typing import Any


FILE_MAP_READ = 0x0004


def _setup_kernel32() -> Any:
    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
    kernel32.OpenFileMappingW.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.LPCWSTR]
    kernel32.OpenFileMappingW.restype = wintypes.HANDLE
    kernel32.MapViewOfFile.argtypes = [
        wintypes.HANDLE,
        wintypes.DWORD,
        wintypes.DWORD,
        wintypes.DWORD,
        ctypes.c_size_t,
    ]
    kernel32.MapViewOfFile.restype = wintypes.LPVOID
    kernel32.UnmapViewOfFile.argtypes = [wintypes.LPCVOID]
    kernel32.UnmapViewOfFile.restype = wintypes.BOOL
    kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
    kernel32.CloseHandle.restype = wintypes.BOOL
    return kernel32


kernel32 = _setup_kernel32()


Vec3 = ctypes.c_float * 3
Vec4 = ctypes.c_float * 4
WheelVec3 = Vec3 * 4
WheelVec4 = Vec4


class CodexVec3(ctypes.Structure):
    _pack_ = 4
    _fields_ = [("x", ctypes.c_float), ("y", ctypes.c_float), ("z", ctypes.c_float)]


class CodexVec4(ctypes.Structure):
    _pack_ = 4
    _fields_ = [("a", ctypes.c_float), ("b", ctypes.c_float), ("c", ctypes.c_float), ("d", ctypes.c_float)]


class CodexPage(ctypes.Structure):
    _pack_ = 4
    _fields_ = [
        ("packetId", ctypes.c_int32),
        ("speedMS", ctypes.c_float),
        ("speedKMH", ctypes.c_float),
        ("gas", ctypes.c_float),
        ("brake", ctypes.c_float),
        ("clutch", ctypes.c_float),
        ("steer", ctypes.c_float),
        ("gear", ctypes.c_int32),
        ("rpm", ctypes.c_int32),
        ("lapCount", ctypes.c_int32),
        ("lapInvalidated", ctypes.c_int32),
        ("lapTime", ctypes.c_int32),
        ("lastLap", ctypes.c_int32),
        ("bestLap", ctypes.c_int32),
        ("performanceMeter", ctypes.c_float),
        ("lastFF", ctypes.c_float),
        ("normalizedSplinePosition", ctypes.c_float),
        ("worldPosition", CodexVec3),
        ("velocity", CodexVec3),
        ("localVelocity", CodexVec3),
        ("localAngularVelocity", CodexVec3),
        ("accG", CodexVec3),
        ("load", CodexVec4),
        ("mz", CodexVec4),
        ("ndSlip", CodexVec4),
        ("slipAngle", CodexVec4),
        ("slipRatio", CodexVec4),
        ("tyreSlip", CodexVec4),
        ("coreTemp", CodexVec4),
        ("camberDeg", CodexVec4),
        ("suspensionTravel", CodexVec4),
        ("wheelAngularSpeed", CodexVec4),
        ("driverName", ctypes.c_char * 64),
        ("carModel", ctypes.c_char * 64),
    ]


class SPageFilePhysics(ctypes.Structure):
    _pack_ = 4
    _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", Vec3),
        ("accG", Vec3),
        ("wheelSlip", Vec4),
        ("wheelLoad", Vec4),
        ("wheelsPressure", Vec4),
        ("wheelAngularSpeed", Vec4),
        ("tyreWear", Vec4),
        ("tyreDirtyLevel", Vec4),
        ("tyreCoreTemperature", Vec4),
        ("camberRAD", Vec4),
        ("suspensionTravel", Vec4),
        ("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", Vec3),
        ("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", Vec4),
        ("clutch", ctypes.c_float),
        ("tyreTempI", Vec4),
        ("tyreTempM", Vec4),
        ("tyreTempO", Vec4),
        ("isAIControlled", ctypes.c_int),
        ("tyreContactPoint", WheelVec3),
        ("tyreContactNormal", WheelVec3),
        ("tyreContactHeading", WheelVec3),
        ("brakeBias", ctypes.c_float),
        ("localVelocity", Vec3),
        ("P2PActivation", ctypes.c_int),
        ("P2PStatus", ctypes.c_int),
        ("currentMaxRpm", ctypes.c_int),
        ("mz", Vec4),
        ("fx", Vec4),
        ("fy", Vec4),
        ("slipRatio", Vec4),
        ("slipAngle", Vec4),
        ("tcInAction", ctypes.c_int),
        ("absInAction", ctypes.c_int),
        ("suspensionDamage", Vec4),
        ("tyreTemp", Vec4),
        ("waterTemp", ctypes.c_float),
        ("brakePressure", Vec4),
        ("frontBrakeCompound", ctypes.c_int),
        ("rearBrakeCompound", ctypes.c_int),
        ("padLife", Vec4),
        ("discLife", Vec4),
        ("ignitionOn", ctypes.c_int),
        ("starterEngineOn", ctypes.c_int),
        ("isEngineRunning", ctypes.c_int),
        ("kerbVibration", ctypes.c_float),
        ("slipVibrations", ctypes.c_float),
        ("gVibrations", ctypes.c_float),
        ("absVibrations", ctypes.c_float),
    ]


class SPageFileGraphic(ctypes.Structure):
    _pack_ = 4
    _fields_ = [
        ("packetId", ctypes.c_int),
        ("status", ctypes.c_int),
        ("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", Vec3),
        ("penaltyTime", ctypes.c_float),
        ("flag", ctypes.c_int),
        ("idealLineOn", ctypes.c_int),
        ("isInPitLane", ctypes.c_int),
        ("surfaceGrip", ctypes.c_float),
        ("mandatoryPitDone", ctypes.c_int),
    ]


class SPageFileStatic(ctypes.Structure):
    _pack_ = 4
    _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", Vec4),
        ("tyreRadius", Vec4),
        ("maxTurboBoost", ctypes.c_float),
        ("deprecated_1", ctypes.c_float),
        ("deprecated_2", ctypes.c_float),
        ("penaltiesEnabled", ctypes.c_int),
        ("aidFuelRate", ctypes.c_float),
        ("aidTireRate", ctypes.c_float),
        ("aidMechanicalDamage", ctypes.c_float),
        ("aidAllowTyreBlankets", ctypes.c_int),
        ("aidStability", ctypes.c_float),
        ("aidAutoClutch", ctypes.c_int),
        ("aidAutoBlip", ctypes.c_int),
        ("hasDRS", ctypes.c_int),
        ("hasERS", ctypes.c_int),
        ("hasKERS", ctypes.c_int),
        ("kersMaxJoules", ctypes.c_float),
        ("engineBrakeSettingsCount", ctypes.c_int),
        ("ersPowerControllerCount", ctypes.c_int),
        ("trackSplineLength", ctypes.c_float),
        ("trackConfiguration", ctypes.c_wchar * 33),
        ("ersMaxJ", ctypes.c_float),
        ("isTimedRace", ctypes.c_int),
        ("hasExtraLap", ctypes.c_int),
        ("carSkin", ctypes.c_wchar * 33),
        ("reversedGridPositions", ctypes.c_int),
        ("pitWindowStart", ctypes.c_int),
        ("pitWindowEnd", ctypes.c_int),
    ]


AC_STATUS = {
    0: "off",
    1: "replay",
    2: "live",
    3: "pause",
}

AC_SESSION = {
    -1: "unknown",
    0: "practice",
    1: "qualify",
    2: "race",
    3: "hotlap",
    4: "time_attack",
    5: "drift",
    6: "drag",
}


def _wide_to_str(value: str) -> str:
    return value.split("\x00", 1)[0].strip()


def _to_list(value: Any) -> list[float]:
    return [float(item) for item in value]


def _plausible_or_none(value: float, *, lower: float, upper: float) -> float | None:
    return value if lower <= value <= upper else None


@dataclass(slots=True)
class NamedSharedMemoryPage:
    name: str
    struct_type: Any
    handle: int | None = None
    view: int | None = None

    def open(self) -> bool:
        if self.handle and self.view:
            return True
        handle = kernel32.OpenFileMappingW(FILE_MAP_READ, False, self.name)
        if not handle:
            return False
        view = kernel32.MapViewOfFile(handle, FILE_MAP_READ, 0, 0, 0)
        if not view:
            kernel32.CloseHandle(handle)
            return False
        self.handle = int(handle)
        self.view = int(view)
        return True

    def close(self) -> None:
        if self.view:
            kernel32.UnmapViewOfFile(ctypes.c_void_p(self.view))
        if self.handle:
            kernel32.CloseHandle(self.handle)
        self.view = None
        self.handle = None

    def _read_once(self) -> Any:
        if not self.view:
            raise RuntimeError(f"{self.name} is not open")
        raw = ctypes.string_at(self.view, ctypes.sizeof(self.struct_type))
        return self.struct_type.from_buffer_copy(raw)

    def read(self, stable: bool = True, attempts: int = 4) -> Any | None:
        if not self.open():
            return None
        if not stable or "packetId" not in dict(self.struct_type._fields_):
            return self._read_once()
        latest = None
        for _ in range(attempts):
            first = self._read_once()
            second = self._read_once()
            latest = second
            if first.packetId == second.packetId:
                return second
            time.sleep(0.001)
        return latest


class AssettoCorsaReader:
    def __init__(self) -> None:
        self.physics = NamedSharedMemoryPage("acpmf_physics", SPageFilePhysics)
        self.graphics = NamedSharedMemoryPage("acpmf_graphics", SPageFileGraphic)
        self.static = NamedSharedMemoryPage("acpmf_static", SPageFileStatic)
        self.codex = NamedSharedMemoryPage("acpmf_Codex", CodexPage)

    def close(self) -> None:
        self.physics.close()
        self.graphics.close()
        self.static.close()
        self.codex.close()

    def connected(self) -> bool:
        return self.physics.open() and self.graphics.open() and self.static.open()

    def read_snapshot(self) -> dict[str, Any] | None:
        if not self.connected():
            return None
        physics = self.physics.read(stable=True)
        graphics = self.graphics.read(stable=True)
        static = self.static.read(stable=False)
        if physics is None or graphics is None or static is None:
            return None
        codex = self.codex.read(stable=True)
        return {
            "captured_at_unix_s": time.time(),
            "packet_id_physics": int(physics.packetId),
            "packet_id_graphics": int(graphics.packetId),
            "status": AC_STATUS.get(int(graphics.status), f"unknown:{graphics.status}"),
            "session_type": AC_SESSION.get(int(graphics.session), f"unknown:{graphics.session}"),
            "car_model": _wide_to_str(static.carModel),
            "track": _wide_to_str(static.track),
            "player_name": " ".join(
                part
                for part in [
                    _wide_to_str(static.playerName),
                    _wide_to_str(static.playerSurname),
                ]
                if part
            )
            or _wide_to_str(static.playerNick),
            "player_nick": _wide_to_str(static.playerNick),
            "completed_laps": int(graphics.completedLaps),
            "position": int(graphics.position),
            "current_lap_time_ms": int(graphics.iCurrentTime),
            "last_lap_time_ms": int(graphics.iLastTime),
            "best_lap_time_ms": int(graphics.iBestTime),
            "current_sector_index": int(graphics.currentSectorIndex),
            "last_sector_time_ms": int(graphics.lastSectorTime),
            "session_time_left_s": float(graphics.sessionTimeLeft),
            "distance_traveled_m": float(graphics.distanceTraveled),
            "normalized_car_position": float(graphics.normalizedCarPosition),
            "is_in_pit": bool(graphics.isInPit),
            "is_in_pit_lane": bool(graphics.isInPitLane),
            "tyre_compound": _wide_to_str(graphics.tyreCompound),
            "surface_grip": float(graphics.surfaceGrip),
            "mandatory_pit_done": bool(getattr(graphics, "mandatoryPitDone", 0)),
            "speed_kmh": float(physics.speedKmh),
            "gear": int(physics.gear),
            "rpm": int(physics.rpms),
            "current_max_rpm": _plausible_or_none(float(physics.currentMaxRpm), lower=1000.0, upper=25000.0),
            "throttle": float(physics.gas),
            "brake": float(physics.brake),
            "clutch": float(physics.clutch),
            "steer_angle_deg": float(physics.steerAngle),
            "heading_rad": float(physics.heading),
            "pitch_rad": float(physics.pitch),
            "roll_rad": float(physics.roll),
            "cg_height_m": float(physics.cgHeight),
            "fuel_l": float(physics.fuel),
            "g_lat": float(physics.accG[0]),
            "g_lon": float(physics.accG[2]),
            "g_vert": float(physics.accG[1]),
            "velocity_mps": _to_list(physics.velocity),
            "local_velocity_mps": _to_list(codex.localVelocity) if codex is not None else _to_list(physics.localVelocity),
            "local_angular_velocity": _to_list(codex.localAngularVelocity) if codex is not None else _to_list(physics.localAngularVel),
            "wheel_slip": _to_list(physics.wheelSlip),
            "wheel_slip_peak": max(abs(v) for v in _to_list(physics.wheelSlip)),
            "wheel_load": _to_list(physics.wheelLoad),
            "wheel_pressure": _to_list(physics.wheelsPressure),
            "wheel_angular_speed": _to_list(physics.wheelAngularSpeed),
            "tyre_wear": _to_list(physics.tyreWear),
            "tyre_core_temp_c": _to_list(physics.tyreCoreTemperature),
            "tyre_temp_i_c": _to_list(physics.tyreTempI),
            "tyre_temp_m_c": _to_list(physics.tyreTempM),
            "tyre_temp_o_c": _to_list(physics.tyreTempO),
            "tyre_temp_c": _to_list(physics.tyreTemp),
            "camber_rad": _to_list(physics.camberRAD),
            "suspension_travel_m": _to_list(physics.suspensionTravel),
            "ride_height_m": _to_list(physics.rideHeight),
            "brake_temp_c": _to_list(physics.brakeTemp),
            "brake_pressure": _to_list(physics.brakePressure),
            "slip_ratio": _to_list(codex.slipRatio) if codex is not None else _to_list(physics.slipRatio),
            "slip_angle_rad": _to_list(codex.slipAngle) if codex is not None else _to_list(physics.slipAngle),
            "tyre_self_align_torque_nm": _to_list(codex.mz) if codex is not None else _to_list(physics.mz),
            "tyre_force_x_n": _to_list(physics.fx),
            "tyre_force_y_n": _to_list(physics.fy),
            "pit_limiter_on": bool(physics.pitLimiterOn),
            "number_of_tyres_out": int(physics.numberOfTyresOut),
            "drs_enabled": bool(physics.drsEnabled),
            "drs_available": bool(physics.drsAvailable),
            "tc_in_action": bool(physics.tcInAction),
            "abs_in_action": bool(physics.absInAction),
            "final_ff": float(physics.finalFF),
            "water_temp_c": _plausible_or_none(float(physics.waterTemp), lower=1.0, upper=200.0),
            "air_temp_c": float(physics.airTemp),
            "road_temp_c": float(physics.roadTemp),
            "air_density": float(physics.airDensity),
            "turbo_boost": float(physics.turboBoost),
            "brake_bias": float(physics.brakeBias),
            "car_damage": _to_list(physics.carDamage),
            "car_coordinates": _to_list(graphics.carCoordinates),
            "penalties_enabled": bool(static.penaltiesEnabled),
            "sector_count": int(static.sectorCount),
            "max_fuel_l": float(static.maxFuel),
            "max_rpm": int(static.maxRpm),
            "max_torque_nm": float(static.maxTorque),
            "max_power_kw": float(static.maxPower),
            "max_turbo_boost": float(static.maxTurboBoost),
            "track_configuration": _wide_to_str(getattr(static, "trackConfiguration", "")),
            "car_skin": _wide_to_str(getattr(static, "carSkin", "")),
            "track_spline_length_m": float(getattr(static, "trackSplineLength", 0.0)),
            "codex_bridge_available": codex is not None,
        }
