using System.Diagnostics;
using System.Runtime.InteropServices;
using NyxCS2.UI;

namespace NyxCS2.Core;

public class Player
{
    public ulong Pawn;
    public ulong Controller;
    public string Name = "";
    public Vec3 Origin;   // feet
    public Vec3 Eye;      // view offset (head height)
    public int Health;
    public int MaxHealth;
    public int Team;
    public uint Ping;
    public float Distance; // ~meters (units / 100)
    public bool Drawable;
    public int Armor;
    public string Weapon = "";
    public Vec3[] Bones;   // skeleton joints (indices 2..41), may be null
}

public class Snapshot
{
    public List<Player> Players = new();
    public Player Local;
    public float[] ViewMatrix = new float[16];
    public bool MatrixValid;
    public bool VmTransposed;   // true when the matrix needs the column-major W2S
    public int W = 1, H = 1;
    public bool Attached;
    public string Status = "waiting for CS2...";
    public string Diag = "";
    public IntPtr GameWindow = IntPtr.Zero;
}

public class Engine
{
    private readonly AppSettings _s;
    private IntPtr _h;
    private int _pid;
    private ulong _client, _engine;
    private IntPtr _gameWnd;
    private bool _run;
    private Thread _thread;

    // runtime calibration (offsets drift between builds; we self-check)
    private uint _pawnHandle = Offsets.PlayerPawn;
    private int _calibPid;
    private int _vmFrames, _vmLastCal = -1, _silentFrames, _lastPlog = -1;
    private readonly string _logPath = Path.Combine(AppContext.BaseDirectory, "nyx.log");

    public Snapshot Snap = new();

    public Engine(AppSettings s) { _s = s; }

    public void Start()
    {
        _run = true;
        _thread = new Thread(Loop) { IsBackground = true };
        _thread.Start();
    }

    public void Stop()
    {
        _run = false;
        _thread?.Join(500);
        if (_h != IntPtr.Zero) { Memory.CloseHandle(_h); _h = IntPtr.Zero; }
        Memory.Handle = IntPtr.Zero;
    }

    private void Loop()
    {
        while (_run)
        {
            try
            {
                if (_h == IntPtr.Zero) Attach();
                if (_h != IntPtr.Zero) Update();
            }
            catch { /* one bad read shouldn't kill the loop */ }
            Thread.Sleep(16);
        }
    }

    private void Attach()
    {
        Snap.Attached = false;
        int pid = 0;
        try
        {
            Process[] ps = Process.GetProcessesByName("cs2");
            if (ps.Length == 0) { Snap.Status = "CS2 not running (start the game first)"; return; }
            pid = ps[0].Id;
            _gameWnd = ps[0].MainWindowHandle;
        }
        catch { return; }

        _h = Memory.OpenProcess(0x0400 | 0x0008 | 0x0010 | 0x0020, false, pid); // QUERY_INFORMATION | VM_OPERATION | VM_READ | VM_WRITE
        if (_h == IntPtr.Zero)
        {
            Snap.Status = "OpenProcess denied -> run this menu as Administrator";
            return;
        }
        Memory.Handle = _h;   // <- this was missing: Memory.Read() reads through the STATIC handle,
                              //    so every game read silently returned 0 (el=0, players=0, no ESP)
        _pid = pid;
        _client = Memory.GetModuleBase(_h, _pid, "client.dll");
        _engine = Memory.GetModuleBase(_h, _pid, "engine2.dll");
        if (_client == 0 || _engine == 0)
        {
            Snap.Status = $"modules not found (client={_client:X} engine={_engine:X}) - in game yet?";
            Memory.CloseHandle(_h); _h = IntPtr.Zero;
            return;
        }
        Snap.Attached = true;
        Snap.GameWindow = _gameWnd;
        Snap.Status = $"attached pid={_pid} client=0x{_client:X} engine=0x{_engine:X} (offsets {Offsets.Source})";
        Log($"attached pid={_pid} client=0x{_client:X} engine=0x{_engine:X} offsets={Offsets.Source}");
    }

    private void Update()
    {
        var snap = new Snapshot { Attached = true, Status = Snap.Status, GameWindow = _gameWnd };

        ulong el = Memory.Read<ulong>(_client + Offsets.EntityList);

        // local pawn: direct read first, then via the local controller (some builds)
        ulong local = Memory.Read<ulong>(_client + Offsets.LocalPlayerPawn);
        if (local == 0 && el != 0)
        {
            ulong lc = Memory.Read<ulong>(_client + Offsets.LocalPlayerController);
            if (lc != 0)
            {
                uint ph = Memory.Read<uint>(lc + _pawnHandle);
                if (ph != 0) local = ResolvePawn(el, ph);
            }
        }
        snap.Local = ReadPlayer(local, 0);

        int w = Memory.Read<int>(_engine + Offsets.WindowWidth);
        int h = Memory.Read<int>(_engine + Offsets.WindowHeight);
        if (w <= 200 || h <= 200)
        {
            var b = Screen.PrimaryScreen.Bounds;
            w = b.Width; h = b.Height;
        }
        snap.W = w; snap.H = h;

        byte[] m = Memory.ReadBytes(_client + Offsets.ViewMatrix, 64);
        if (m != null)
        {
            for (int i = 0; i < 16; i++) snap.ViewMatrix[i] = BitConverter.ToSingle(m, i * 4);
            snap.MatrixValid = snap.ViewMatrix[15] != 0f;
        }

        // view-matrix orientation: keep re-checking, never trust a single frame.
        _vmFrames++;
        if (snap.MatrixValid && snap.Local != null && snap.Local.Pawn != 0 && _vmLastCal != _vmFrames / 240)
        {
            _vmLastCal = _vmFrames / 240;
            Vec3 origin = snap.Local.Origin;
            bool std = InBounds(origin, snap.ViewMatrix, snap.W, snap.H, false);
            bool trs = !std && InBounds(origin, snap.ViewMatrix, snap.W, snap.H, true);
            if (std) snap.VmTransposed = false;
            else if (trs) snap.VmTransposed = true;
            // both failed -> keep the previous choice (mid-loading frame)
        }

        if (_calibPid != _pid && snap.Local != null && snap.Local.Pawn != 0)
            CalibratePawnHandle(snap.Local.Pawn);

        var players = new List<Player>();
        int controllers = 0, handles = 0, pawns = 0;
        if (el != 0)
        {
            for (int i = 1; i < Offsets.MaxPlayers; i++)
            {
                ulong le1 = Memory.Read<ulong>(el + (((ulong)(i & 0x7FFF) >> 9) * 8) + Offsets.ListEntryBase);
                if (le1 == 0) continue;
                ulong ctrl = Memory.Read<ulong>(le1 + ((ulong)(i & 0x1FF)) * Offsets.ControllerStride);
                if (ctrl == 0) continue;
                controllers++;
                uint pawnHandle = Memory.Read<uint>(ctrl + _pawnHandle);
                if (pawnHandle == 0) continue;
                handles++;
                ulong pawn = ResolvePawn(el, pawnHandle);
                if (pawn == 0) continue;
                pawns++;
                if (local != 0 && pawn == local) continue;   // self (only when we know local)
                Player pl = ReadPlayer(pawn, ctrl);
                if (pl == null) continue;
                if (pl.Team < 2 || pl.Team > 3) continue;    // not a player
                if (pl.Health <= 0 || pl.Health > 100) continue; // dead / invalid
                if (snap.Local != null && _s.EnemiesOnly && pl.Team == snap.Local.Team) continue;

                pl.Distance = snap.Local != null ? Vec3.Distance(snap.Local.Origin, pl.Origin) / 100f : 0f;
                if (!float.IsFinite(pl.Distance)) pl.Distance = 0f;
                if (_s.MaxDistance > 0 && pl.Distance > _s.MaxDistance) continue;

                pl.Drawable = true;
                players.Add(pl);
            }
        }

        // diagnostic string so the status line can tell us exactly where it breaks
        string diag = el == 0 ? "no-entity-list(el=0)"
            : controllers == 0 ? $"el=0x{el:X} c0"
            : handles == 0 ? $"el=0x{el:X} c={controllers} h0"
            : pawns == 0 ? $"el=0x{el:X} c={controllers} h={handles} p0"
            : $"el=0x{el:X} c={controllers} h={handles} p={pawns}";
        snap.Diag = diag;

        if (players.Count == 0 && (++_silentFrames % 300) == 0)
            Log($"diag pid={_pid} {diag} local=0x{local:X} handle=0x{_pawnHandle:X} vm={snap.VmTransposed}");
        if (players.Count > 0 && _lastPlog != _pid)
        {
            _lastPlog = _pid;
            Log($"players={players.Count} drawable (local=0x{local:X} vm={(snap.VmTransposed ? "transposed" : "std")} w={snap.W} h={snap.H})");
        }

        snap.Players = players;
        Snap = snap;

        if (_s.AimEnabled) AimbotStep(snap);
        if (_s.Triggerbot) TriggerbotStep(snap);
        if (_s.AntiFlash && snap.Local != null && snap.Local.Pawn != 0)
            AntiFlash(snap.Local.Pawn);
    }

    private void Log(string s)
    {
        try
        {
            if (File.Exists(_logPath) && new FileInfo(_logPath).Length > 200_000) File.Delete(_logPath);
            File.AppendAllText(_logPath, $"[{DateTime.Now:HH:mm:ss.fff}] {s}\r\n");
        }
        catch { }
    }

    private Player ReadPlayer(ulong pawn, ulong ctrl)
    {
        if (pawn == 0) return null;
        var pl = new Player
        {
            Pawn = pawn,
            Controller = ctrl,
            Health = Memory.Read<int>(pawn + Offsets.Health),
            MaxHealth = Math.Max(Memory.Read<int>(pawn + Offsets.MaxHealth), 1),
            Team = Memory.Read<byte>(pawn + Offsets.TeamNum),
            Origin = Memory.Read<Vec3>(pawn + Offsets.OldOrigin),
            Eye = Memory.Read<Vec3>(pawn + Offsets.ViewOffset),
            Name = ctrl != 0 ? Memory.ReadUtf8String(ctrl + Offsets.SanitizedName, 48) : "",
            Ping = ctrl != 0 ? Memory.Read<uint>(ctrl + Offsets.Ping) : 0,
            Armor = Math.Clamp(Memory.Read<int>(pawn + Offsets.Armor), 0, 100),
            Weapon = _s.WeaponEsp ? ReadWeaponName(pawn) : "",
            Bones = (_s.Skeleton || _s.AimEnabled) ? ReadBones(pawn) : null,
        };
        // garbage-read guard: non-finite vectors would crash GDI+ during drawing
        if (!Math3D.IsFinite(pl.Origin) || !Math3D.IsFinite(pl.Eye)) return null;
        pl.Eye = new Vec3(
            Math.Clamp(pl.Eye.X, -300f, 300f),
            Math.Clamp(pl.Eye.Y, -300f, 300f),
            Math.Clamp(pl.Eye.Z, -300f, 300f));
        return pl;
    }

    // active weapon -> weapon entity -> econ item view -> item definition index -> name.
    // Chain (build 14183): pawn.m_pWeaponServices -> .m_hActiveWeapon (handle)
    //   -> entity-list resolve -> weapon.m_AttributeManager -> .m_Item -> .m_iItemDefinitionIndex
    private string ReadWeaponName(ulong pawn)
    {
        try
        {
            ulong svc = Memory.Read<ulong>(pawn + Offsets.WeaponServices);
            if (svc == 0) return "";
            uint hw = Memory.Read<uint>(svc + Offsets.ActiveWeapon);
            if (hw == 0 || hw == 0xFFFFFFFF) return "";   // no weapon (dead/holstered)
            ulong el = Memory.Read<ulong>(_client + Offsets.EntityList);
            ulong weapon = ResolvePawn(el, hw);
            if (weapon == 0) return "";
            ulong am = Memory.Read<ulong>(weapon + Offsets.AttributeManager);
            if (am == 0) return "";
            ulong item = Memory.Read<ulong>(am + Offsets.Item);
            if (item == 0) return "";
            ushort def = Memory.Read<ushort>(item + Offsets.ItemDefinitionIndex);
            return GameData.WeaponName(def);
        }
        catch { return ""; }
    }

    // skeleton bone matrix: pawn.m_pGameSceneNode -> .m_modelState -> .m_boneArray.
    // Each bone = 48 bytes: 3x4 matrix from a quat + position in the LAST
    // column (bytes 12,16,20) - the first 12 bytes are the rotation rows and
    // are NOT usable as a position (that is why bones read as zero/garbage
    // before). Same layout as every other CS2 external menu.
    private Vec3[] ReadBones(ulong pawn)
    {
        try
        {
            ulong scene = Memory.Read<ulong>(pawn + Offsets.GameSceneNode);
            if (scene == 0) return null;
            ulong ms = Memory.Read<ulong>(scene + Offsets.ModelState);
            if (ms == 0) return null;
            ulong arr = Memory.Read<ulong>(ms + Offsets.BoneArray);
            if (arr == 0) return null;
            byte[] buf = Memory.ReadBytes(arr, 42 * 48);
            if (buf == null) return null;
            var bones = new Vec3[42];   // default = (0,0,0) so unused joints draw nothing
            for (int b = 2; b < 42; b++)
            {
                int off = (b * 48) + 12;   // position column of the bone matrix
                var v = new Vec3(
                    BitConverter.ToSingle(buf, off),
                    BitConverter.ToSingle(buf, off + 4),
                    BitConverter.ToSingle(buf, off + 8));
                bones[b] = Math3D.IsFinite(v) ? v : default;
            }
            return bones;
        }
        catch { return null; }
    }

    // pawn handle (uint) -> pawn pointer, using the page scheme + current stride
    private ulong ResolvePawn(ulong entityList, uint handle)
    {
        ulong le = Memory.Read<ulong>(entityList + (((ulong)(handle & 0x7FFF) >> 9) * 8) + Offsets.ListEntryBase);
        if (le == 0) return 0;
        return Memory.Read<ulong>(le + ((ulong)(handle & 0x1FF)) * Offsets.PawnStride);
    }

    private static bool InBounds(Vec3 world, float[] m, int w, int h, bool transposed)
    {
        if (!Math3D.WorldToScreen(world, m, w, h, out Vec2 s, transposed)) return false;
        return s.X > -80f && s.X < w + 80f && s.Y > -80f && s.Y < h + 80f;
    }

    // pick the controller->pawn handle field that resolves to OUR OWN pawn
    // (both m_hPlayerPawn 0x92C and m_hPawn 0x6BC exist; the correct one is
    // whichever leads to the same pawn as dwLocalPlayerPawn).
    private void CalibratePawnHandle(ulong localPawn)
    {
        _calibPid = _pid;
        ulong el = Memory.Read<ulong>(_client + Offsets.EntityList);
        if (el == 0) return;

        ulong[] candidates = { Offsets.PlayerPawn, Offsets.PlayerPawnAlt };
        for (int i = 1; i < Offsets.MaxPlayers; i++)
        {
            ulong le1 = Memory.Read<ulong>(el + (((ulong)(i & 0x7FFF) >> 9) * 8) + Offsets.ListEntryBase);
            if (le1 == 0) continue;
            ulong ctrl = Memory.Read<ulong>(le1 + ((ulong)(i & 0x1FF)) * Offsets.ControllerStride);
            if (ctrl == 0) continue;
            foreach (uint cand in candidates)
            {
                uint h = Memory.Read<uint>(ctrl + cand);
                if (h == 0) continue;
                if (ResolvePawn(el, h) == localPawn)
                {
                    _pawnHandle = cand;
                    Log($"pawnHandle calibrated to 0x{cand:X} (localPawn=0x{localPawn:X})");
                    return;
                }
            }
        }
        // not found -> keep the default, try again next attach
        _calibPid = 0;
    }

    // ---- aim: pick best target by angle-FOV, then steer the view angles onto its
    // head by writing dwViewAngles (the field the game itself writes/reads each
    // frame). This is the standard external technique - it works even with raw
    // mouse input, because we move the camera, not the cursor.
    private void AimbotStep(Snapshot snap)
    {
        if (snap.Local == null || snap.Local.Pawn == 0) return;
        if (snap.Local.Health <= 0) return;          // dead - no aim
        if (!AimKeyHeld()) return;
        if (_gameWnd != IntPtr.Zero && GetForegroundWindow() != _gameWnd) return;

        Vec3 localEyes = snap.Local.Origin + snap.Local.Eye;

        Vec3 bestHead = default; Player best = null; float bestFov = _s.AimFov;
        foreach (Player pl in snap.Players)
        {
            if (pl.Team == snap.Local.Team) continue;
            Vec3 head = AimPoint(pl);
            Math3D.CalcAngle(localEyes, head, out Vec3 ang);
            float d = MathF.Sqrt(ang.X * ang.X + ang.Y * ang.Y);
            if (d < bestFov) { bestFov = d; best = pl; bestHead = head; }
        }
        if (best == null) return;

        // current view angles (the values the game is using right now)
        Vec3 cur = Memory.Read<Vec3>(_client + Offsets.ViewAngles);
        if (!Math3D.IsFinite(cur)) return;

        Math3D.CalcAngle(localEyes, AimPoint(best), out Vec3 aim);

        // smooth toward the target angle via shortest signed deltas
        float k = 1f / Math.Max(_s.AimSmooth, 1f);
        float newPitch = cur.X + Math3D.AngleDiff(cur.X, aim.X) * k;
        float newYaw = cur.Y + Math3D.AngleDiff(cur.Y, aim.Y) * k;
        newPitch = Math.Clamp(newPitch, -89f, 89f);

        Memory.Write(_client + Offsets.ViewAngles, new Vec3(newPitch, newYaw, 0f));
    }

    private bool AimKeyHeld()
    {
        return _s.AimKey switch
        {
            1 => true,                             // always
            2 => (GetAsyncKeyState(0x02) & 0x8000) != 0, // right mouse
            3 => (GetAsyncKeyState(0x12) & 0x8000) != 0, // alt
            _ => false,
        };
    }

    private long _nextTrigger;

    // pick the target point for the aim position setting (Head / Chest / Legs).
    // Prefers exact skeleton joints when they were read; falls back to the
    // feet+eye heuristic so aiming still works even if bones failed to read.
    private Vec3 AimPoint(Player p)
    {
        bool head = _s.AimPos == 0, chest = _s.AimPos == 1; // 2 = legs/body
        if (p.Bones != null)
        {
            int[] want = head ? new[] { 6, 5 } : chest ? new[] { 4, 3 } : new[] { 2, 14, 21 };
            foreach (int b in want)
            {
                if (b < 0 || b >= p.Bones.Length) continue;
                Vec3 v = p.Bones[b];
                if (v.X != 0f || v.Y != 0f || v.Z != 0f) return v;
            }
        }
        // fallback heuristics (no bones / dead)
        Vec3 feet = p.Origin;
        if (head) return new Vec3(feet.X, feet.Y, feet.Z + 66f);
        if (chest) return new Vec3(feet.X, feet.Y, feet.Z + 44f);
        return feet;
    }

    // Triggerbot: fires a real left-click when the crosshair (m_iIDEntIndex) is
    // resting on an alive enemy while the trigger key is held. Uses the same
    // entity-list page scheme as the player loop to resolve index -> controller.
    private void TriggerbotStep(Snapshot snap)
    {
        if (snap.Local == null || snap.Local.Pawn == 0) return;
        if (snap.Local.Health <= 0) return;
        if (!TriggerKeyHeld()) return;
        if (_gameWnd != IntPtr.Zero && GetForegroundWindow() != _gameWnd) return;

        int idx = Memory.Read<int>(snap.Local.Pawn + Offsets.IdEntIndex);
        if (idx < 0 || idx >= 0x7FFF) return;

        ulong el = Memory.Read<ulong>(_client + Offsets.EntityList);
        if (el == 0) return;
        ulong le = Memory.Read<ulong>(el + ((((ulong)idx & 0x7FFF) >> 9) * 8) + Offsets.ListEntryBase);
        if (le == 0) return;
        ulong ctrl = Memory.Read<ulong>(le + ((ulong)(idx & 0x1FF)) * Offsets.ControllerStride);
        if (ctrl == 0) return;

        Player t = null;
        foreach (Player p in snap.Players)
        {
            if (p.Controller == ctrl && p.Pawn != 0) { t = p; break; }
        }
        if (t == null || t.Team == snap.Local.Team || t.Health <= 0) return;

        long now = Environment.TickCount64;
        if (now < _nextTrigger) return;
        _nextTrigger = now + Math.Max(_s.TriggerDelay, 25);
        MouseClick();
    }

    private bool TriggerKeyHeld()
    {
        return _s.TriggerKey switch
        {
            1 => true,                                    // always
            2 => (GetAsyncKeyState(0x02) & 0x8000) != 0, // right mouse
            3 => (GetAsyncKeyState(0x12) & 0x8000) != 0, // alt
            _ => false,
        };
    }

    // Anti-flash: while enabled, zero the local pawn's flash duration so a
    // detonated flashbang has no whiteout effect.
    private void AntiFlash(ulong pawn)
    {
        float flash = Memory.Read<float>(pawn + Offsets.FlashDuration);
        if (flash > 0f)
            Memory.Write(pawn + Offsets.FlashDuration, 0f);
    }

    // Real left-click via SendInput (works with raw input, consistent with the
    // angle-write aimbot approach).
    private void MouseClick()
    {
        var ip = new INPUT();
        ip.type = 0;                       // 0 = mouse input
        ip.U.mi = new MOUSEINPUT { dwFlags = 0x0002 }; // MOUSEEVENTF_LEFTDOWN
        SendInput(1, ref ip, Marshal.SizeOf<INPUT>());
        ip.U.mi = new MOUSEINPUT { dwFlags = 0x0004 }; // MOUSEEVENTF_LEFTUP
        SendInput(1, ref ip, Marshal.SizeOf<INPUT>());
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct INPUT { public int type; public InputUnion U; }

    [StructLayout(LayoutKind.Explicit)]
    private struct InputUnion
    {
        [FieldOffset(0)] public MOUSEINPUT mi;
        [FieldOffset(0)] public KEYBDINPUT ki;
        [FieldOffset(0)] public HARDWAREINPUT hi;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct MOUSEINPUT
    {
        public int dx; public int dy; public int mouseData;
        public int dwFlags; public int time; public IntPtr dwExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct KEYBDINPUT
    {
        public ushort wVk; public ushort wScan; public uint dwFlags;
        public int time; public IntPtr dwExtraInfo;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct HARDWAREINPUT
    {
        public int uMsg; public short wParamL; public short wParamH;
    }

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint SendInput(uint nInputs, ref INPUT pInputs, int cbSize);

    [DllImport("user32.dll")]
    private static extern short GetAsyncKeyState(int vKey);
    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();
}