using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using NyxCS2.Core;

namespace NyxCS2.UI;

// Transparent, click-through, always-on-top drawing surface over the game.
// Follows the game window every tick so W2S coordinates always line up.
public class OverlayForm : Form
{
    private readonly Func<Snapshot> _snap;
    private readonly AppSettings _s;
    private readonly System.Windows.Forms.Timer _tick;
    private DateTime _lastPaintLog = DateTime.MinValue;

    // ---- window anchoring ----
    private struct RECT { public int Left, Top, Right, Bottom; }
    private struct POINT { public int X, Y; }
    [DllImport("user32.dll")] private static extern bool GetClientRect(IntPtr h, out RECT r);
    [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr h, ref POINT p);
    [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr h);
    [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
    private static readonly IntPtr HWND_TOPMOST = new(-1);
    private const uint SWP_NOMOVE = 0x2, SWP_NOSIZE = 0x1, SWP_NOACTIVATE = 0x10;

    public OverlayForm(Func<Snapshot> snap, AppSettings s)
    {
        _snap = snap;
        _s = s;
        FormBorderStyle = FormBorderStyle.None;
        TopMost = true;
        ShowInTaskbar = false;
        BackColor = Color.Magenta;
        TransparencyKey = Color.Magenta;
        Bounds = Screen.PrimaryScreen.Bounds;
        DoubleBuffered = true;
        _tick = new System.Windows.Forms.Timer { Interval = 16 };
        _tick.Tick += (_, _) => { SnapToGame(); Invalidate(); };
        _tick.Start();
    }

    // keep the overlay exactly over the game's client area (windowed/borderless safe)
    private void SnapToGame()
    {
        try
        {
            IntPtr hwnd = _snap().GameWindow;
            if (hwnd == IntPtr.Zero || IsIconic(hwnd)) return;
            if (!GetClientRect(hwnd, out RECT rc) || rc.Right - rc.Left <= 0) return;
            var p = new POINT { X = 0, Y = 0 };
            ClientToScreen(hwnd, ref p);
            var b = new Rectangle(p.X, p.Y, rc.Right - rc.Left, rc.Bottom - rc.Top);
            if (b != Bounds)
            {
                SetBounds(b.X, b.Y, b.Width, b.Height);
                SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
            }
        }
        catch { }
    }

    public void Toggle()
    {
        if (InvokeRequired) { BeginInvoke(new Action(Toggle)); return; }
        Visible = !Visible;
    }

    // containment: an exception raised while the OS processes WM_SIZE
    // (0x0005) must never take the overlay down - this is exactly the
    // "exception processing message" crash that killed the old builds.
    protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
    {
        try { base.SetBoundsCore(x, y, width, height, specified); }
        catch (Exception ex) { PaintLog("resize", ex); }
    }

    protected override void OnResize(EventArgs e)
    {
        try { base.OnResize(e); }
        catch (Exception ex) { PaintLog("resize", ex); }
    }

    private void PaintLog(string tag, Exception ex)
    {
        if ((DateTime.UtcNow - _lastPaintLog).TotalSeconds < 30) return;
        _lastPaintLog = DateTime.UtcNow;
        try
        {
            File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "nyx.log"),
                $"[{DateTime.Now:HH:mm:ss}] {tag} error: {ex.Message}\r\n");
        }
        catch { }
    }

    protected override CreateParams CreateParams
    {
        get
        {
            var cp = base.CreateParams;
            cp.ExStyle |= 0x00000020;   // WS_EX_TRANSPARENT (click-through)
            cp.ExStyle |= 0x08000000;   // WS_EX_NOACTIVATE
            cp.ExStyle |= 0x00000080;   // WS_EX_TOOLWINDOW
            return cp;
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        if (e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Visible = false; return; }
        _tick.Stop();
        base.OnFormClosing(e);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        try
        {
            Snapshot snap = _snap();
            if (snap == null) return;

            Graphics g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;

            int w = Math.Max(snap.W, 1), h = Math.Max(snap.H, 1);

            if (_s.Crosshair)
            {
                using var pen = new Pen(Color.FromArgb(180, Color.White), 1.6f);
                g.DrawLine(pen, w / 2f - 8f, h / 2f, w / 2f + 8f, h / 2f);
                g.DrawLine(pen, w / 2f, h / 2f - 8f, w / 2f, h / 2f + 8f);
            }

            if (_s.EspEnabled && snap.MatrixValid)
            {
                foreach (Player p in snap.Players)
                {
                    if (!p.Drawable) continue;
                    DrawPlayer(g, p, snap);
                }
            }

            // status line (top-left)
            Color statusColor = Color.FromArgb(200, snap.Attached ? Color.Lime : Color.Orange);
            using var f = new Font("Consolas", 10f);
            using var sf = new SolidBrush(statusColor);
            string line = $"{snap.Status} | players={snap.Players.Count} | INSERT=overlay END=menu";
            g.DrawString(line, f, sf, 8f, 6f);

            using var f2 = new Font("Consolas", 8.5f);
            using var sf2 = new SolidBrush(Color.FromArgb(170, Color.Gray));
            g.DrawString($"v{NyxCS2.Program.Version} | offsets: {Offsets.Source} | vm:{(snap.VmTransposed ? "transposed" : "std")} | {snap.Diag}",
                f2, sf2, 8f, 22f);

            if (_s.ShowFovCircle && _s.AimEnabled) DrawFovCircle(g, snap, w, h);
            if (_s.ShowPlayerList) DrawPlayerList(g, snap);
        }
        catch (Exception ex)
        {
            // never let a paint glitch kill the overlay - log it once in a while
            if ((DateTime.UtcNow - _lastPaintLog).TotalSeconds > 30)
            {
                _lastPaintLog = DateTime.UtcNow;
                try
                {
                    File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "nyx.log"),
                        $"[{DateTime.Now:HH:mm:ss}] paint error (skipped frame): {ex.Message}\r\n");
                }
                catch { }
            }
        }
    }

    // aim FOV circle at the crosshair - radius derived from the projection
    // scale (m[5]) so it matches the actual aim-FOV degrees you set.
    private void DrawFovCircle(Graphics g, Snapshot snap, int w, int h)
    {
        float scale = MathF.Abs(snap.ViewMatrix[5]);
        if (!float.IsFinite(scale) || scale <= 0.1f || scale > 10f) return;   // no usable matrix
        float radius = (h / 2f) * scale * MathF.Tan(_s.AimFov / 2f * MathF.PI / 180f);
        if (!float.IsFinite(radius) || radius < 3f || radius > h) return;
        using var pen = new Pen(Color.FromArgb(170, Color.Cyan), 1.4f);
        g.DrawEllipse(pen, w / 2f - radius, h / 2f - radius, radius * 2f, radius * 2f);
    }

    // compact player list panel, top-right (name / hp / distance, team-colored)
    private void DrawPlayerList(Graphics g, Snapshot snap)
    {
        var players = snap.Players;
        if (players == null || players.Count == 0) return;
        var sorted = players.OrderBy(pl => pl.Distance).Take(20).ToList();
        float x = snap.W - 248f, y = 6f;
        using var bg = new SolidBrush(Color.FromArgb(150, Color.Black));
        using var font = new Font("Consolas", 9f);
        g.FillRectangle(bg, x, y, 240f, 20f + sorted.Count * 16f);
        g.DrawString("PLAYERS", font, Brushes.SkyBlue, x + 6f, y + 3f);
        for (int i = 0; i < sorted.Count; i++)
        {
            var pl = sorted[i];
            Color c = pl.Team == snap.Local?.Team ? Color.LimeGreen : Color.Red;
            using var col = new SolidBrush(c);
            float ry = y + 22f + i * 16f;
            string name = pl.Name.Length > 18 ? pl.Name[..18] : pl.Name;
            g.DrawString(name, font, col, x + 6f, ry);
            g.DrawString($"{(pl.Health > 0 ? pl.Health + "hp" : "dead")}", font, Brushes.White, x + 132f, ry);
            g.DrawString($"{pl.Distance:F0}m", font, Brushes.Gray, x + 196f, ry);
        }
    }

    private void DrawPlayer(Graphics g, Player p, Snapshot snap)
    {
        Vec3 feet = p.Origin;
        Vec3 head = new Vec3(feet.X, feet.Y, feet.Z + Offsets.PlayerHeight);

        if (!Math3D.WorldToScreen(feet, snap.ViewMatrix, snap.W, snap.H, out Vec2 sf, snap.VmTransposed)) return;
        if (!Math3D.WorldToScreen(head, snap.ViewMatrix, snap.W, snap.H, out Vec2 sh, snap.VmTransposed)) return;

        if (sh.Y > sf.Y) return; // behind-ish / flipped

        float boxH = MathF.Abs(sf.Y - sh.Y);
        // degenerate/insane geometry -> skip the player. GDI+ hard-crashes
        // the process on NaN/Infinity, so never let garbage reach it.
        if (!float.IsFinite(boxH) || !float.IsFinite(sf.X) || !float.IsFinite(sf.Y)) return;
        if (boxH < 2f || boxH > snap.H * 4f) return;
        if (sf.X < -20000f || sf.X > snap.W + 20000f || sh.X < -20000f || sh.X > snap.W + 20000f) return;
        if (sf.Y < -20000f || sf.Y > snap.H + 40000f || sh.Y < -40000f || sh.Y > snap.H + 20000f) return;

        float boxW = MathF.Min(boxH * 0.55f, 800f);
        float left = sf.X - boxW / 2f, top = sh.Y;

        Color team = p.Team == snap.Local?.Team ? Color.LimeGreen : Color.Red;

        using var boxPen = new Pen(team, 1.6f);
        if (_s.Boxes) g.DrawRectangle(boxPen, left, top, boxW, boxH);

        if (_s.HealthBar)
        {
            float pct = Math.Clamp((float)p.Health / p.MaxHealth, 0f, 1f);
            using var hb = new SolidBrush(Color.FromArgb(220, LerpColor(Color.Red, Color.LimeGreen, pct)));
            g.FillRectangle(hb, left - 5f, top, 3f, boxH);
            using var frame = new Pen(Color.FromArgb(120, Color.Black), 1f);
            g.DrawRectangle(frame, left - 5f, top, 3f, boxH);
        }

        if (_s.ShowName || _s.ShowDistance)
        {
            using var nameFont = new Font("Segoe UI", 9.5f, FontStyle.Bold);
            string txt = "";
            if (_s.ShowName) txt = p.Name;
            if (_s.ShowDistance) txt += (txt.Length > 0 ? "  " : "") + $"{p.Distance:F0}m";
            if (txt.Length > 0)
            {
                SizeF sz = g.MeasureString(txt, nameFont);
                float tx = left + boxW / 2f - sz.Width / 2f;
                using var bg = new SolidBrush(Color.FromArgb(140, Color.Black));
                g.FillRectangle(bg, tx - 2f, top - sz.Height - 2f, sz.Width + 4f, sz.Height + 2f);
                g.DrawString(txt, nameFont, Brushes.White, tx, top - sz.Height - 1f);
            }
        }

        if (_s.Skeleton && p.Bones != null)
        {
            using var bonePen = new Pen(Color.FromArgb(200, team), 1.4f);
            foreach (var (a, b) in NyxCS2.Core.GameData.SkeletonLinks)
            {
                if (a < 0 || b < 0 || a >= p.Bones.Length || b >= p.Bones.Length) continue;
                Vec3 ba = p.Bones[a], bb = p.Bones[b];
                if (ba.X == 0f && ba.Y == 0f && ba.Z == 0f) continue;
                if (bb.X == 0f && bb.Y == 0f && bb.Z == 0f) continue;
                if (!Math3D.WorldToScreen(ba, snap.ViewMatrix, snap.W, snap.H, out Vec2 sa, snap.VmTransposed)) continue;
                if (!Math3D.WorldToScreen(bb, snap.ViewMatrix, snap.W, snap.H, out Vec2 sb, snap.VmTransposed)) continue;
                if (!float.IsFinite(sa.X) || !float.IsFinite(sa.Y) || !float.IsFinite(sb.X) || !float.IsFinite(sb.Y)) continue;
                g.DrawLine(bonePen, sa.X, sa.Y, sb.X, sb.Y);
            }
        }

        if (_s.Snapline)
        {
            using var linePen = new Pen(Color.FromArgb(140, team), 1.2f);
            g.DrawLine(linePen, snap.W / 2f, snap.H, sf.X, sf.Y);
        }
    }

    private static Color LerpColor(Color a, Color b, float t)
    {
        if (!float.IsFinite(t)) t = 0f;
        t = Math.Clamp(t, 0f, 1f);
        return Color.FromArgb(
            ClampByte(a.R + (b.R - a.R) * t),
            ClampByte(a.G + (b.G - a.G) * t),
            ClampByte(a.B + (b.B - a.B) * t));
    }

    private static int ClampByte(float v)
    {
        if (v < 0f) return 0;
        if (v > 255f) return 255;
        return (int)v;
    }
}