using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;

namespace MCPForUnity.Runtime.Helpers
//The reason for having another Runtime Utilities in additional to Editor Utilities is to avoid Editor-only dependencies in this runtime code.
{
    public readonly struct ScreenshotCaptureResult
    {
        public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize)
            : this(fullPath, projectRelativePath, superSize, isAsync: false, imageBase64: null, imageWidth: 0, imageHeight: 0)
        {
        }

        public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize, bool isAsync)
            : this(fullPath, projectRelativePath, superSize, isAsync, imageBase64: null, imageWidth: 0, imageHeight: 0)
        {
        }

        public ScreenshotCaptureResult(string fullPath, string projectRelativePath, int superSize, bool isAsync,
            string imageBase64, int imageWidth, int imageHeight)
        {
            FullPath = fullPath;
            ProjectRelativePath = projectRelativePath;
            SuperSize = superSize;
            IsAsync = isAsync;
            ImageBase64 = imageBase64;
            ImageWidth = imageWidth;
            ImageHeight = imageHeight;
        }

        public string FullPath { get; }
        /// <summary>Path relative to the Unity project root (e.g. "Captures/foo.png"). Suitable for ScreenCapture.CaptureScreenshot.</summary>
        public string ProjectRelativePath { get; }
        public int SuperSize { get; }
        public bool IsAsync { get; }
        /// <summary>Base64-encoded PNG image data. Only populated when include_image is true.</summary>
        public string ImageBase64 { get; }
        public int ImageWidth { get; }
        public int ImageHeight { get; }
    }

    public static class ScreenshotUtility
    {
        /// <summary>
        /// Built-in default folder for screenshots, project-relative.
        /// Callers can override per-call via the <c>folderOverride</c> parameter on capture methods,
        /// or globally via <c>ScreenshotPreferences</c> in the Editor assembly.
        /// </summary>
        public const string DefaultFolder = "Assets/Screenshots";

        private static Camera FindAvailableCamera()
        {
            var main = Camera.main;
            if (main != null)
            {
                return main;
            }

            try
            {
                var cams = UnityFindObjectsCompat.FindAll<Camera>();
                return cams.FirstOrDefault();
            }
            catch
            {
                return null;
            }
        }

        public static ScreenshotCaptureResult CaptureToProjectFolder(
            string fileName = null,
            int superSize = 1,
            bool ensureUniqueFileName = true,
            string folderOverride = null)
        {
            ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride, isAsync: true);
            // ScreenCapture.CaptureScreenshot accepts paths relative to the project root.
            ScreenCapture.CaptureScreenshot(result.ProjectRelativePath, result.SuperSize);
            return result;
        }

        /// <summary>
        /// Captures a screenshot from a specific camera by rendering into a temporary RenderTexture (works in Edit Mode).
        /// When <paramref name="includeImage"/> is true, the result includes a base64-encoded PNG (optionally
        /// downscaled so the longest edge is at most <paramref name="maxResolution"/>).
        /// </summary>
        public static ScreenshotCaptureResult CaptureFromCameraToProjectFolder(
            Camera camera,
            string fileName = null,
            int superSize = 1,
            bool ensureUniqueFileName = true,
            bool includeImage = false,
            int maxResolution = 0,
            string folderOverride = null)
        {
            if (camera == null)
            {
                throw new ArgumentNullException(nameof(camera));
            }

            ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride, isAsync: false);
            int size = result.SuperSize;

            int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
            int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);
            width *= size;
            height *= size;

            RenderTexture prevRT = camera.targetTexture;
            RenderTexture prevActive = RenderTexture.active;
            var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
            Texture2D tex = null;
            Texture2D downscaled = null;
            string imageBase64 = null;
            int imgW = 0, imgH = 0;
            try
            {
                camera.targetTexture = rt;
                camera.Render();

                RenderTexture.active = rt;
                tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();

                byte[] png = tex.EncodeToPNG();
                File.WriteAllBytes(result.FullPath, png);

                if (includeImage)
                {
                    int targetMax = maxResolution > 0 ? maxResolution : 640;
                    if (width > targetMax || height > targetMax)
                    {
                        downscaled = DownscaleTexture(tex, targetMax);
                        byte[] smallPng = downscaled.EncodeToPNG();
                        imageBase64 = System.Convert.ToBase64String(smallPng);
                        imgW = downscaled.width;
                        imgH = downscaled.height;
                    }
                    else
                    {
                        imageBase64 = System.Convert.ToBase64String(png);
                        imgW = width;
                        imgH = height;
                    }
                }
            }
            finally
            {
                camera.targetTexture = prevRT;
                RenderTexture.active = prevActive;
                RenderTexture.ReleaseTemporary(rt);
                DestroyTexture(tex);
                DestroyTexture(downscaled);
            }

            if (includeImage && imageBase64 != null)
            {
                return new ScreenshotCaptureResult(
                    result.FullPath, result.ProjectRelativePath, result.SuperSize, false,
                    imageBase64, imgW, imgH);
            }
            return result;
        }

#if UNITY_EDITOR
        // Synchronously drive a WaitForEndOfFrame ScreenshotCapturer by pumping the editor's
        // player loop. Play-mode only; EditorApplication.Step is a no-op in edit mode.
        private static Texture2D CaptureCompositedAfterFrame(int superSize, int timeoutSteps = 5)
        {
            Texture2D result = null;
            bool done = false;
            bool callerReturned = false;
            ScreenshotCapturer.Begin(superSize, tex =>
            {
                // Late completion after the spin loop timed out: caller will never consume
                // the texture, so destroy it here to avoid leaking a Unity object.
                if (callerReturned)
                {
                    if (tex != null) DestroyTexture(tex);
                    return;
                }
                result = tex;
                done = true;
            });
            // Step() pauses play mode as a side effect; restore the prior state so a screenshot
            // doesn't leave a running game paused (an already-paused game stays paused).
            bool wasPaused = UnityEditor.EditorApplication.isPaused;
            try
            {
                for (int i = 0; i < timeoutSteps && !done; i++)
                {
                    UnityEditor.EditorApplication.Step();
                }
            }
            finally
            {
                if (!wasPaused)
                    UnityEditor.EditorApplication.isPaused = false;
            }
            callerReturned = true;
            return result;
        }
#endif

        /// <summary>
        /// Captures a screenshot using ScreenCapture.CaptureScreenshotAsTexture, which captures the
        /// final composited frame including UI Toolkit overlays, post-processing, etc.
        /// Falls back to camera-based capture if ScreenCapture returns null at runtime.
        /// </summary>
        public static ScreenshotCaptureResult CaptureComposited(
            string fileName = null,
            int superSize = 1,
            bool ensureUniqueFileName = true,
            bool includeImage = false,
            int maxResolution = 0,
            string folderOverride = null)
        {
            ScreenshotCaptureResult result = PrepareCaptureResult(fileName, superSize, ensureUniqueFileName, folderOverride: folderOverride, isAsync: false);
            Texture2D tex = null;
            Texture2D downscaled = null;
            string imageBase64 = null;
            int imgW = 0, imgH = 0;
            try
            {
#if UNITY_EDITOR
                // In play mode, inline ScreenCapture reads a backbuffer before UITK has
                // composited; route through WaitForEndOfFrame instead.
                tex = Application.isPlaying
                    ? CaptureCompositedAfterFrame(result.SuperSize)
                    : ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
#else
                tex = ScreenCapture.CaptureScreenshotAsTexture(result.SuperSize);
#endif
                if (tex == null)
                {
                    // Fallback to camera-based if ScreenCapture fails
                    var cam = FindAvailableCamera();
                    if (cam != null)
                        return CaptureFromCameraToProjectFolder(cam, fileName, superSize, ensureUniqueFileName,
                            includeImage, maxResolution, folderOverride: folderOverride);
                    throw new InvalidOperationException("ScreenCapture.CaptureScreenshotAsTexture returned null and no fallback camera available.");
                }

                int width = tex.width;
                int height = tex.height;

                byte[] png = tex.EncodeToPNG();
                File.WriteAllBytes(result.FullPath, png);

                if (includeImage)
                {
                    int targetMax = maxResolution > 0 ? maxResolution : 640;
                    if (width > targetMax || height > targetMax)
                    {
                        downscaled = DownscaleTexture(tex, targetMax);
                        byte[] smallPng = downscaled.EncodeToPNG();
                        imageBase64 = System.Convert.ToBase64String(smallPng);
                        imgW = downscaled.width;
                        imgH = downscaled.height;
                    }
                    else
                    {
                        imageBase64 = System.Convert.ToBase64String(png);
                        imgW = width;
                        imgH = height;
                    }
                }
            }
            finally
            {
                DestroyTexture(tex);
                DestroyTexture(downscaled);
            }

            if (includeImage && imageBase64 != null)
            {
                return new ScreenshotCaptureResult(
                    result.FullPath, result.ProjectRelativePath, result.SuperSize, false,
                    imageBase64, imgW, imgH);
            }
            return result;
        }

        /// <summary>
        /// Renders a camera to a Texture2D without saving to disk. Used for multi-angle captures.
        /// Returns the base64-encoded PNG, downscaled to fit within <paramref name="maxResolution"/>.
        /// </summary>
        public static (string base64, int width, int height) RenderCameraToBase64(Camera camera, int maxResolution = 640)
        {
            if (camera == null) throw new ArgumentNullException(nameof(camera));

            int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
            int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);

            RenderTexture prevRT = camera.targetTexture;
            RenderTexture prevActive = RenderTexture.active;
            var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
            Texture2D tex = null;
            Texture2D downscaled = null;
            try
            {
                camera.targetTexture = rt;
                camera.Render();

                RenderTexture.active = rt;
                tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();

                int targetMax = maxResolution > 0 ? maxResolution : 640;
                if (width > targetMax || height > targetMax)
                {
                    downscaled = DownscaleTexture(tex, targetMax);
                    string b64 = System.Convert.ToBase64String(downscaled.EncodeToPNG());
                    return (b64, downscaled.width, downscaled.height);
                }
                else
                {
                    string b64 = System.Convert.ToBase64String(tex.EncodeToPNG());
                    return (b64, width, height);
                }
            }
            finally
            {
                camera.targetTexture = prevRT;
                RenderTexture.active = prevActive;
                RenderTexture.ReleaseTemporary(rt);
                DestroyTexture(tex);
                DestroyTexture(downscaled);
            }
        }

        /// <summary>
        /// Renders a camera to a Texture2D without saving to disk.
        /// Caller owns the returned texture and must destroy it.
        /// </summary>
        public static Texture2D RenderCameraToTexture(Camera camera, int maxResolution = 640)
        {
            if (camera == null) throw new ArgumentNullException(nameof(camera));

            int width = Mathf.Max(1, camera.pixelWidth > 0 ? camera.pixelWidth : Screen.width);
            int height = Mathf.Max(1, camera.pixelHeight > 0 ? camera.pixelHeight : Screen.height);

            RenderTexture prevRT = camera.targetTexture;
            RenderTexture prevActive = RenderTexture.active;
            var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
            Texture2D tex = null;
            try
            {
                camera.targetTexture = rt;
                camera.Render();

                RenderTexture.active = rt;
                tex = new Texture2D(width, height, TextureFormat.RGBA32, false);
                tex.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                tex.Apply();

                int targetMax = maxResolution > 0 ? maxResolution : 640;
                if (width > targetMax || height > targetMax)
                {
                    var downscaled = DownscaleTexture(tex, targetMax);
                    DestroyTexture(tex);
                    tex = null;
                    return downscaled;
                }
                var result = tex;
                tex = null; // transfer ownership to caller
                return result;
            }
            finally
            {
                camera.targetTexture = prevRT;
                RenderTexture.active = prevActive;
                RenderTexture.ReleaseTemporary(rt);
                DestroyTexture(tex);
            }
        }

        /// <summary>
        /// Composites a list of tile textures into a single contact-sheet grid image.
        /// Labels are drawn as white text on a dark banner at the bottom of each tile.
        /// Returns base64 PNG plus dimensions. Destroys all input tile textures.
        /// </summary>
        public static (string base64, int width, int height) ComposeContactSheet(
            List<Texture2D> tiles, List<string> labels, int padding = 4)
        {
            if (tiles == null || tiles.Count == 0)
                throw new ArgumentException("No tiles to compose.", nameof(tiles));

            int tileW = tiles[0].width;
            int tileH = tiles[0].height;
            int count = tiles.Count;

            // Calculate grid: prefer wider than tall (cols >= rows)
            int cols = Mathf.CeilToInt(Mathf.Sqrt(count));
            int rows = Mathf.CeilToInt((float)count / cols);

            int labelHeight = Mathf.Max(14, tileH / 12);
            int cellW = tileW + padding;
            int cellH = tileH + labelHeight + padding;

            int sheetW = cols * cellW + padding;
            int sheetH = rows * cellH + padding;

            Texture2D sheet = null;
            try
            {
                sheet = new Texture2D(sheetW, sheetH, TextureFormat.RGBA32, false);

                // Build the full sheet in a Color32[] buffer, then upload once
                var bgColor = new Color32(30, 30, 30, 255);
                Color32[] sheetPixels = new Color32[sheetW * sheetH];
                for (int i = 0; i < sheetPixels.Length; i++) sheetPixels[i] = bgColor;

                // Track label draw requests so we can apply them after the bulk upload
                var labelDraws = new List<(string text, int x, int y, int h)>();

                for (int idx = 0; idx < count; idx++)
                {
                    int col = idx % cols;
                    int row = idx / cols;
                    // Place tiles top-left to bottom-right (Unity Texture2D y=0 is bottom)
                    int x = padding + col * cellW;
                    int y = sheetH - padding - (row + 1) * cellH + padding;

                    // Copy tile pixels row-by-row using bulk operations
                    Color32[] tilePixels = tiles[idx].GetPixels32();
                    for (int ty = 0; ty < tileH; ty++)
                    {
                        int srcOffset = ty * tileW;
                        int dstOffset = (y + labelHeight + ty) * sheetW + x;
                        System.Array.Copy(tilePixels, srcOffset, sheetPixels, dstOffset, tileW);
                    }

                    // Draw label banner (dark background strip below tile)
                    var bannerColor = new Color32(20, 20, 20, 220);
                    for (int ly = 0; ly < labelHeight; ly++)
                    {
                        int dstOffset = (y + ly) * sheetW + x;
                        for (int lx = 0; lx < tileW; lx++)
                        {
                            sheetPixels[dstOffset + lx] = bannerColor;
                        }
                    }

                    // Queue label text drawing (applied after bulk pixel upload)
                    if (labels != null && idx < labels.Count && !string.IsNullOrEmpty(labels[idx]))
                    {
                        labelDraws.Add((labels[idx], x + 3, y + 2, labelHeight - 4));
                    }
                }

                // Upload all tile + banner pixels in one SetPixels32 call
                sheet.SetPixels32(sheetPixels);

                // Draw label text on top (small glyph-based writes, negligible cost)
                foreach (var (text, lx, ly, lh) in labelDraws)
                {
                    DrawText(sheet, text, lx, ly, lh, Color.white);
                }

                sheet.Apply();

                byte[] png = sheet.EncodeToPNG();
                string b64 = System.Convert.ToBase64String(png);
                return (b64, sheetW, sheetH);
            }
            finally
            {
                foreach (var tile in tiles) DestroyTexture(tile);
                DestroyTexture(sheet);
            }
        }

        private static void DrawText(Texture2D tex, string text, int startX, int startY, int charHeight, Color color)
        {
            // Simple 5x7 bitmap font for basic ASCII characters
            int charWidth = Mathf.Max(4, charHeight * 5 / 7);
            int spacing = Mathf.Max(1, charWidth / 5);
            int x = startX;

            foreach (char c in text)
            {
                if (x + charWidth > tex.width) break;
                ulong glyph = GetGlyph(c);
                if (glyph != 0)
                {
                    for (int row = 0; row < 7; row++)
                    {
                        for (int col = 0; col < 5; col++)
                        {
                            bool on = ((glyph >> ((6 - row) * 5 + (4 - col))) & 1) == 1;
                            if (!on) continue;
                            // Scale the 5x7 glyph to charWidth x charHeight
                            int px0 = x + col * charWidth / 5;
                            int px1 = x + (col + 1) * charWidth / 5;
                            int py0 = startY + (6 - row) * charHeight / 7;
                            int py1 = startY + (7 - row) * charHeight / 7;
                            for (int py = py0; py < py1 && py < tex.height; py++)
                                for (int px = px0; px < px1 && px < tex.width; px++)
                                    tex.SetPixel(px, py, color);
                        }
                    }
                }
                x += charWidth + spacing;
            }
        }

        private static ulong GetGlyph(char c)
        {
            // 5x7 pixel font stored as 35-bit values (row0=bits34-30 ... row6=bits4-0)
            // Each row is 5 wide, MSB=left. Row 0 is top.
            switch (char.ToUpperInvariant(c))
            {
                case 'A': return 0b01110_10001_10001_11111_10001_10001_10001UL;
                case 'B': return 0b11110_10001_10001_11110_10001_10001_11110UL;
                case 'C': return 0b01110_10001_10000_10000_10000_10001_01110UL;
                case 'D': return 0b11100_10010_10001_10001_10001_10010_11100UL;
                case 'E': return 0b11111_10000_10000_11110_10000_10000_11111UL;
                case 'F': return 0b11111_10000_10000_11110_10000_10000_10000UL;
                case 'G': return 0b01110_10001_10000_10111_10001_10001_01110UL;
                case 'H': return 0b10001_10001_10001_11111_10001_10001_10001UL;
                case 'I': return 0b01110_00100_00100_00100_00100_00100_01110UL;
                case 'K': return 0b10001_10010_10100_11000_10100_10010_10001UL;
                case 'L': return 0b10000_10000_10000_10000_10000_10000_11111UL;
                case 'M': return 0b10001_11011_10101_10101_10001_10001_10001UL;
                case 'N': return 0b10001_11001_10101_10011_10001_10001_10001UL;
                case 'O': return 0b01110_10001_10001_10001_10001_10001_01110UL;
                case 'R': return 0b11110_10001_10001_11110_10100_10010_10001UL;
                case 'S': return 0b01110_10001_10000_01110_00001_10001_01110UL;
                case 'T': return 0b11111_00100_00100_00100_00100_00100_00100UL;
                case 'U': return 0b10001_10001_10001_10001_10001_10001_01110UL;
                case 'V': return 0b10001_10001_10001_10001_01010_01010_00100UL;
                case 'W': return 0b10001_10001_10001_10101_10101_11011_10001UL;
                case 'Y': return 0b10001_10001_01010_00100_00100_00100_00100UL;
                case '0': return 0b01110_10011_10101_10101_10101_11001_01110UL;
                case '1': return 0b00100_01100_00100_00100_00100_00100_01110UL;
                case '2': return 0b01110_10001_00001_00010_00100_01000_11111UL;
                case '3': return 0b01110_10001_00001_00110_00001_10001_01110UL;
                case '4': return 0b00010_00110_01010_10010_11111_00010_00010UL;
                case '5': return 0b11111_10000_11110_00001_00001_10001_01110UL;
                case '6': return 0b01110_10001_10000_11110_10001_10001_01110UL;
                case '7': return 0b11111_00001_00010_00100_01000_01000_01000UL;
                case '8': return 0b01110_10001_10001_01110_10001_10001_01110UL;
                case '9': return 0b01110_10001_10001_01111_00001_10001_01110UL;
                case 'J': return 0b00111_00010_00010_00010_00010_10010_01100UL;
                case 'P': return 0b11110_10001_10001_11110_10000_10000_10000UL;
                case 'Q': return 0b01110_10001_10001_10001_10101_10010_01101UL;
                case 'X': return 0b10001_01010_00100_00100_00100_01010_10001UL;
                case 'Z': return 0b11111_00001_00010_00100_01000_10000_11111UL;
                case '-': return 0b00000_00000_00000_11111_00000_00000_00000UL;
                case '_': return 0b00000_00000_00000_00000_00000_00000_11111UL;
                case ' ': return 0UL;
                case '+': return 0b00000_00100_00100_11111_00100_00100_00000UL;
                default:  return 0UL;
            }
        }

        /// <summary>
        /// Downscales a Texture2D so that its longest edge is at most <paramref name="maxEdge"/> pixels.
        /// Uses bilinear filtering via a temporary RenderTexture blit.
        /// Caller must destroy the returned Texture2D.
        /// </summary>
        public static Texture2D DownscaleTexture(Texture2D source, int maxEdge)
        {
            if (source == null)
                throw new System.ArgumentNullException(nameof(source));
            if (maxEdge <= 0)
                throw new System.ArgumentOutOfRangeException(nameof(maxEdge), maxEdge, "maxEdge must be > 0.");

            int srcW = source.width;
            int srcH = source.height;
            float scale = Mathf.Min((float)maxEdge / srcW, (float)maxEdge / srcH);
            scale = Mathf.Min(scale, 1f); // never upscale
            int dstW = Mathf.Max(1, Mathf.RoundToInt(srcW * scale));
            int dstH = Mathf.Max(1, Mathf.RoundToInt(srcH * scale));

            RenderTexture prevActive = RenderTexture.active;
            var rt = RenderTexture.GetTemporary(dstW, dstH, 0, RenderTextureFormat.ARGB32);
            rt.filterMode = FilterMode.Bilinear;
            try
            {
                Graphics.Blit(source, rt);
                RenderTexture.active = rt;
                var dst = new Texture2D(dstW, dstH, TextureFormat.RGBA32, false);
                dst.ReadPixels(new Rect(0, 0, dstW, dstH), 0, 0);
                dst.Apply();
                return dst;
            }
            finally
            {
                RenderTexture.active = prevActive;
                RenderTexture.ReleaseTemporary(rt);
            }
        }

        private static void DestroyTexture(Texture2D tex)
        {
            if (tex == null) return;
            if (Application.isPlaying)
                UnityEngine.Object.Destroy(tex);
            else
                UnityEngine.Object.DestroyImmediate(tex);
        }

        private static ScreenshotCaptureResult PrepareCaptureResult(string fileName, int superSize, bool ensureUniqueFileName, string folderOverride, bool isAsync)
        {
            int size = Mathf.Max(1, superSize);
            string resolvedName = BuildFileName(fileName);
            string folderAbsolute = ResolveFolderAbsolute(folderOverride);
            Directory.CreateDirectory(folderAbsolute);

            string fullPath = Path.Combine(folderAbsolute, resolvedName);
            if (ensureUniqueFileName)
            {
                fullPath = EnsureUnique(fullPath);
            }

            string normalizedFullPath = fullPath.Replace('\\', '/');
            string projectRelativePath = ToProjectRelativePath(normalizedFullPath);

            return new ScreenshotCaptureResult(normalizedFullPath, projectRelativePath, size, isAsync);
        }

        /// <summary>
        /// Resolves a folder spec (which may be project-relative like "Assets/Screenshots",
        /// absolute, or null/empty) to an absolute filesystem path inside the project root.
        /// Throws on traversal escape or absolute paths outside the project.
        /// </summary>
        public static string ResolveFolderAbsolute(string folderOverride)
        {
            string projectRoot = GetProjectRootPath().TrimEnd('/');
            string requested = string.IsNullOrWhiteSpace(folderOverride) ? DefaultFolder : folderOverride.Trim();
            requested = requested.Replace('\\', '/').TrimEnd('/');

            string combined = Path.IsPathRooted(requested)
                ? requested
                : Path.Combine(projectRoot, requested);

            string fullFolder = Path.GetFullPath(combined).Replace('\\', '/').TrimEnd('/');
            string normalizedRoot = projectRoot;

            // Reject paths that escape the project root (case-insensitive on Windows, exact elsewhere).
            var rootComparison = Application.platform == RuntimePlatform.WindowsEditor
                ? StringComparison.OrdinalIgnoreCase
                : StringComparison.Ordinal;
            if (!fullFolder.Equals(normalizedRoot, rootComparison) &&
                !fullFolder.StartsWith(normalizedRoot + "/", rootComparison))
            {
                throw new InvalidOperationException(
                    $"Screenshot folder '{folderOverride}' resolves outside the Unity project root ('{fullFolder}'). " +
                    $"Use a project-relative path (e.g. 'Assets/Screenshots' or 'Captures').");
            }

            return fullFolder;
        }

        /// <summary>
        /// Converts an absolute filesystem path inside the project to a project-relative path
        /// (forward slashes, no leading separator). Returns the input unchanged when it does
        /// not live under the project root.
        /// </summary>
        public static string ToProjectRelativePath(string normalizedFullPath)
        {
            if (string.IsNullOrEmpty(normalizedFullPath)) return normalizedFullPath;
            string projectRoot = GetProjectRootPath();
            string normalized = normalizedFullPath.Replace('\\', '/');
            if (normalized.StartsWith(projectRoot, StringComparison.OrdinalIgnoreCase))
            {
                return normalized.Substring(projectRoot.Length).TrimStart('/');
            }
            return normalized;
        }

        /// <summary>
        /// True when <paramref name="projectRelativePath"/> lives under the Unity Assets/ folder
        /// (and therefore should be imported via AssetDatabase).
        /// </summary>
        public static bool IsUnderAssets(string projectRelativePath)
        {
            if (string.IsNullOrEmpty(projectRelativePath)) return false;
            string norm = projectRelativePath.Replace('\\', '/').TrimStart('/');
            return norm.Equals("Assets", StringComparison.OrdinalIgnoreCase)
                || norm.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase);
        }

        private static string BuildFileName(string fileName)
        {
            string name = string.IsNullOrWhiteSpace(fileName)
                ? $"screenshot-{DateTime.Now:yyyyMMdd-HHmmss}"
                : fileName.Trim();

            name = SanitizeFileName(name);

            if (!name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) &&
                !name.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) &&
                !name.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
            {
                name += ".png";
            }

            return name;
        }

        private static string SanitizeFileName(string fileName)
        {
            // GetInvalidFileNameChars() doesn't include '\' or '/' on Unix, so a caller-supplied
            // name like "foo\bar" would survive and later get spliced into the directory portion.
            var invalidChars = Path.GetInvalidFileNameChars();
            string cleaned = new string(
                fileName.Select(ch => invalidChars.Contains(ch) || ch == '/' || ch == '\\' ? '_' : ch).ToArray());

            return string.IsNullOrWhiteSpace(cleaned) ? "screenshot" : cleaned;
        }

        private static string EnsureUnique(string path)
        {
            if (!File.Exists(path))
            {
                return path;
            }

            string directory = Path.GetDirectoryName(path) ?? string.Empty;
            string baseName = Path.GetFileNameWithoutExtension(path);
            string extension = Path.GetExtension(path);
            int counter = 1;

            string candidate;
            do
            {
                candidate = Path.Combine(directory, $"{baseName}-{counter}{extension}");
                counter++;
            } while (File.Exists(candidate));

            return candidate;
        }

        private static string GetProjectRootPath()
        {
            string root = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
            root = root.Replace('\\', '/');
            if (!root.EndsWith("/", StringComparison.Ordinal))
            {
                root += "/";
            }
            return root;
        }
    }

    /// <summary>
    /// Transient MonoBehaviour that yields WaitForEndOfFrame, calls
    /// ScreenCapture.CaptureScreenshotAsTexture, invokes the callback, and self-destructs.
    /// </summary>
    public sealed class ScreenshotCapturer : MonoBehaviour
    {
        private int _superSize = 1;
        private Action<Texture2D> _onComplete;

        /// <summary>Spawns a hidden GameObject, attaches a capturer, returns immediately.</summary>
        public static void Begin(int superSize, Action<Texture2D> onComplete)
        {
            var go = new GameObject("__MCP_ScreenshotCapturer__") { hideFlags = HideFlags.HideAndDontSave };
            var c = go.AddComponent<ScreenshotCapturer>();
            c._superSize = Mathf.Max(1, superSize);
            c._onComplete = onComplete;
        }

        private System.Collections.IEnumerator Start()
        {
            yield return new WaitForEndOfFrame();
            Texture2D tex = null;
            try { tex = ScreenCapture.CaptureScreenshotAsTexture(_superSize); }
            catch (Exception ex) { Debug.LogError($"[MCP for Unity] CaptureScreenshotAsTexture failed: {ex.Message}"); }
            _onComplete?.Invoke(tex);
            Destroy(gameObject);
        }
    }
}
