using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace UniRun.EditorTools
{
    [InitializeOnLoad]
    internal static class UnityCodexBridgeServer
    {
        private const int PreferredPort = 19744;
        private const int MaxPortAttempts = 5;
        private const string AutoStartPrefKey = "UniRun.CodexBridge.AutoStart";
        private const int MaxLogs = 250;

        private static readonly ConcurrentQueue<BridgeJob> Jobs = new ConcurrentQueue<BridgeJob>();
        private static readonly List<LogEntry> Logs = new List<LogEntry>();
        private static readonly object LogLock = new object();

        private static TcpListener _listener;
        private static Thread _serverThread;
        private static bool _running;
        private static int _port = PreferredPort;
        private static int _logIndex;

        static UnityCodexBridgeServer()
        {
            if (IsBackgroundEditorProcess())
                return;

            Application.logMessageReceived -= OnLogMessageReceived;
            Application.logMessageReceived += OnLogMessageReceived;

            EditorApplication.update -= ProcessJobs;
            EditorApplication.update += ProcessJobs;

            EditorApplication.quitting -= StopServer;
            EditorApplication.quitting += StopServer;

            AssemblyReloadEvents.beforeAssemblyReload -= StopServer;
            AssemblyReloadEvents.beforeAssemblyReload += StopServer;

            if (EditorPrefs.GetBool(AutoStartPrefKey, true))
                StartServer();
        }

        [MenuItem("Tools/Codex Bridge/Start Server")]
        private static void StartServerMenu()
        {
            StartServer();
        }

        [MenuItem("Tools/Codex Bridge/Stop Server")]
        private static void StopServerMenu()
        {
            StopServer();
        }

        [MenuItem("Tools/Codex Bridge/Auto Start")]
        private static void ToggleAutoStart()
        {
            bool enabled = !EditorPrefs.GetBool(AutoStartPrefKey, true);
            EditorPrefs.SetBool(AutoStartPrefKey, enabled);

            if (enabled)
                StartServer();
        }

        [MenuItem("Tools/Codex Bridge/Auto Start", true)]
        private static bool ValidateToggleAutoStart()
        {
            Menu.SetChecked("Tools/Codex Bridge/Auto Start", EditorPrefs.GetBool(AutoStartPrefKey, true));
            return true;
        }

        [MenuItem("Tools/Codex Bridge/Capture Game View")]
        private static void CaptureGameViewMenu()
        {
            BridgeResponse response = CaptureGameView(new Dictionary<string, string>());
            Debug.Log("[CodexBridge] Capture result: " + response.Body);
        }

        private static void StartServer()
        {
            if (IsBackgroundEditorProcess())
                return;

            if (_running)
                return;

            try
            {
                _listener = CreateListener();
                _running = true;

                _serverThread = new Thread(ServerLoop)
                {
                    IsBackground = true,
                    Name = "UnityCodexBridgeServer"
                };
                _serverThread.Start();

                Debug.Log("[CodexBridge] Listening on http://127.0.0.1:" + _port);
            }
            catch (Exception ex)
            {
                _running = false;
                try
                {
                    if (_listener != null)
                        _listener.Stop();
                }
                catch
                {
                    // Ignore cleanup failures after a failed bind.
                }
                finally
                {
                    _listener = null;
                }

                Debug.LogWarning("[CodexBridge] Failed to start: " + ex.Message);
            }
        }

        private static void StopServer()
        {
            _running = false;

            try
            {
                if (_listener != null)
                    _listener.Stop();
            }
            catch
            {
                // Ignore shutdown races.
            }

            _listener = null;

            if (_serverThread != null && _serverThread.IsAlive)
                _serverThread.Join(200);

            _serverThread = null;
        }

        private static TcpListener CreateListener()
        {
            Exception lastException = null;

            for (int i = 0; i < MaxPortAttempts; i++)
            {
                int port = PreferredPort + i;
                TcpListener listener = null;

                try
                {
                    listener = new TcpListener(IPAddress.Loopback, port);
                    listener.Server.ExclusiveAddressUse = true;
                    listener.Start();
                    _port = port;
                    return listener;
                }
                catch (Exception ex)
                {
                    lastException = ex;

                    try
                    {
                        if (listener != null)
                            listener.Stop();
                    }
                    catch
                    {
                        // Ignore cleanup failures while trying fallback ports.
                    }
                }
            }

            throw lastException ?? new SocketException();
        }

        private static bool IsBackgroundEditorProcess()
        {
            string commandLine = Environment.CommandLine;

            return Application.isBatchMode ||
                   commandLine.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0 ||
                   commandLine.IndexOf("-batchMode", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        private static void ServerLoop()
        {
            while (_running)
            {
                try
                {
                    TcpClient client = _listener.AcceptTcpClient();
                    ThreadPool.QueueUserWorkItem(_ => HandleClient(client));
                }
                catch
                {
                    if (_running)
                        Thread.Sleep(100);
                }
            }
        }

        private static void HandleClient(TcpClient client)
        {
            using (client)
            {
                try
                {
                    client.ReceiveTimeout = 5000;
                    client.SendTimeout = 5000;

                    BridgeRequest request = ReadRequest(client.GetStream());
                    BridgeResponse response;

                    if (request == null)
                    {
                        response = BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"invalid_request\"}");
                    }
                    else if (request.Method == "OPTIONS")
                    {
                        response = BridgeResponse.Json(204, string.Empty);
                    }
                    else
                    {
                        BridgeJob job = new BridgeJob(request);
                        Jobs.Enqueue(job);

                        if (!job.Done.Wait(TimeSpan.FromSeconds(10)))
                            response = BridgeResponse.Json(504, "{\"ok\":false,\"error\":\"unity_main_thread_timeout\"}");
                        else
                            response = job.Response;
                    }

                    WriteResponse(client.GetStream(), response);
                }
                catch (Exception ex)
                {
                    try
                    {
                        WriteResponse(client.GetStream(),
                            BridgeResponse.Json(500, "{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}"));
                    }
                    catch
                    {
                        // Client has already gone away.
                    }
                }
            }
        }

        private static BridgeRequest ReadRequest(Stream stream)
        {
            StreamReader reader = new StreamReader(stream, Encoding.UTF8, false, 4096, true);
            string requestLine = reader.ReadLine();
            if (string.IsNullOrEmpty(requestLine))
                return null;

            string[] requestParts = requestLine.Split(' ');
            if (requestParts.Length < 2)
                return null;

            int contentLength = 0;
            string line;
            while (!string.IsNullOrEmpty(line = reader.ReadLine()))
            {
                int separatorIndex = line.IndexOf(':');
                if (separatorIndex <= 0)
                    continue;

                string headerName = line.Substring(0, separatorIndex).Trim();
                string headerValue = line.Substring(separatorIndex + 1).Trim();
                if (string.Equals(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase))
                    int.TryParse(headerValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out contentLength);
            }

            string body = string.Empty;
            if (contentLength > 0)
            {
                char[] buffer = new char[contentLength];
                int read = reader.ReadBlock(buffer, 0, contentLength);
                body = new string(buffer, 0, read);
            }

            Uri uri = new Uri("http://127.0.0.1" + requestParts[1]);
            return new BridgeRequest(requestParts[0].ToUpperInvariant(), uri.AbsolutePath, ParseQuery(uri.Query), body);
        }

        private static Dictionary<string, string> ParseQuery(string query)
        {
            Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            if (string.IsNullOrEmpty(query))
                return values;

            string trimmed = query[0] == '?' ? query.Substring(1) : query;
            string[] pairs = trimmed.Split('&');

            foreach (string pair in pairs)
            {
                if (string.IsNullOrEmpty(pair))
                    continue;

                int separatorIndex = pair.IndexOf('=');
                string key = separatorIndex >= 0 ? pair.Substring(0, separatorIndex) : pair;
                string value = separatorIndex >= 0 ? pair.Substring(separatorIndex + 1) : string.Empty;
                values[Uri.UnescapeDataString(key)] = Uri.UnescapeDataString(value.Replace("+", " "));
            }

            return values;
        }

        private static void WriteResponse(Stream stream, BridgeResponse response)
        {
            if (response == null)
                response = BridgeResponse.Json(500, "{\"ok\":false,\"error\":\"null_response\"}");

            byte[] body = Encoding.UTF8.GetBytes(response.Body ?? string.Empty);
            string headers =
                "HTTP/1.1 " + response.StatusCode + " " + StatusText(response.StatusCode) + "\r\n" +
                "Content-Type: " + response.ContentType + "; charset=utf-8\r\n" +
                "Content-Length: " + body.Length.ToString(CultureInfo.InvariantCulture) + "\r\n" +
                "Access-Control-Allow-Origin: http://localhost\r\n" +
                "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" +
                "Access-Control-Allow-Headers: Content-Type\r\n" +
                "Connection: close\r\n\r\n";

            byte[] headerBytes = Encoding.UTF8.GetBytes(headers);
            stream.Write(headerBytes, 0, headerBytes.Length);
            if (body.Length > 0)
                stream.Write(body, 0, body.Length);
        }

        private static string StatusText(int statusCode)
        {
            switch (statusCode)
            {
                case 200: return "OK";
                case 204: return "No Content";
                case 400: return "Bad Request";
                case 404: return "Not Found";
                case 405: return "Method Not Allowed";
                case 500: return "Internal Server Error";
                case 504: return "Gateway Timeout";
                default: return "OK";
            }
        }

        private static void ProcessJobs()
        {
            while (Jobs.TryDequeue(out BridgeJob job))
            {
                try
                {
                    job.Response = Execute(job.Request);
                }
                catch (Exception ex)
                {
                    job.Response = BridgeResponse.Json(500,
                        "{\"ok\":false,\"error\":" + JsonString(ex.Message) + "}");
                }
                finally
                {
                    job.Done.Set();
                }
            }
        }

        private static BridgeResponse Execute(BridgeRequest request)
        {
            switch (request.Path)
            {
                case "/health":
                case "/state":
                    return GetHealth();
                case "/capture":
                    return CaptureGameView(request.Query);
                case "/logs":
                    return GetLogs(request.Query);
                case "/scene/roots":
                    return GetSceneRoots();
                case "/scene/objects":
                    return GetSceneObjects(request.Query);
                case "/object":
                    return GetObjectDetails(request.Query);
                case "/play":
                    return SetPlayState(true, false);
                case "/pause":
                    return SetPlayState(true, true);
                case "/stop":
                    return SetPlayState(false, false);
                case "/transform":
                    if (request.Method != "POST")
                        return BridgeResponse.Json(405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
                    return SetTransform(request.Body);
                default:
                    return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"not_found\"}");
            }
        }

        private static BridgeResponse GetHealth()
        {
            Scene scene = SceneManager.GetActiveScene();
            string body =
                "{\"ok\":true" +
                ",\"bridge\":\"unity-codex-bridge\"" +
                ",\"port\":" + _port.ToString(CultureInfo.InvariantCulture) +
                ",\"unityVersion\":" + JsonString(Application.unityVersion) +
                ",\"projectPath\":" + JsonString(Directory.GetCurrentDirectory()) +
                ",\"scene\":" + JsonString(scene.IsValid() ? scene.name : string.Empty) +
                ",\"isPlaying\":" + Bool(EditorApplication.isPlaying) +
                ",\"isPaused\":" + Bool(EditorApplication.isPaused) +
                "}";

            return BridgeResponse.Json(200, body);
        }

        private static BridgeResponse CaptureGameView(Dictionary<string, string> query)
        {
            int width = GetInt(query, "width", 1280, 320, 4096);
            int height = GetInt(query, "height", 720, 180, 4096);
            string relativePath = "Captures/latest.png";
            string absolutePath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), relativePath));

            Directory.CreateDirectory(Path.GetDirectoryName(absolutePath));

            Camera camera = ResolveCaptureCamera();
            string cameraName = camera != null ? camera.name : string.Empty;
            bool usedCameraRender = false;

            if (camera != null)
            {
                RenderTexture previousTarget = camera.targetTexture;
                RenderTexture previousActive = RenderTexture.active;
                RenderTexture renderTexture = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
                Texture2D texture = null;

                try
                {
                    camera.targetTexture = renderTexture;
                    RenderTexture.active = renderTexture;
                    camera.Render();

                    texture = new Texture2D(width, height, TextureFormat.RGB24, false);
                    texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
                    texture.Apply();

                    File.WriteAllBytes(absolutePath, texture.EncodeToPNG());
                    usedCameraRender = true;
                }
                finally
                {
                    camera.targetTexture = previousTarget;
                    RenderTexture.active = previousActive;
                    RenderTexture.ReleaseTemporary(renderTexture);
                    if (texture != null)
                        UnityEngine.Object.DestroyImmediate(texture);
                }
            }

            if (!usedCameraRender)
                ScreenCapture.CaptureScreenshot(relativePath);

            string body =
                "{\"ok\":true" +
                ",\"path\":" + JsonString(absolutePath) +
                ",\"relativePath\":" + JsonString(relativePath) +
                ",\"width\":" + width.ToString(CultureInfo.InvariantCulture) +
                ",\"height\":" + height.ToString(CultureInfo.InvariantCulture) +
                ",\"camera\":" + JsonString(cameraName) +
                ",\"mode\":" + JsonString(usedCameraRender ? "camera_render" : "screen_capture") +
                "}";

            return BridgeResponse.Json(200, body);
        }

        private static Camera ResolveCaptureCamera()
        {
            Camera main = Camera.main;
            if (main != null && main.isActiveAndEnabled)
                return main;

            Camera[] cameras = UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
            foreach (Camera camera in cameras)
            {
                if (camera != null && camera.isActiveAndEnabled)
                    return camera;
            }

            return main != null ? main : (cameras.Length > 0 ? cameras[0] : null);
        }

        private static BridgeResponse GetLogs(Dictionary<string, string> query)
        {
            int count = GetInt(query, "count", 80, 1, MaxLogs);
            List<LogEntry> snapshot;

            lock (LogLock)
            {
                int start = Mathf.Max(0, Logs.Count - count);
                snapshot = Logs.GetRange(start, Logs.Count - start);
            }

            StringBuilder sb = new StringBuilder();
            sb.Append("{\"ok\":true,\"logs\":[");

            for (int i = 0; i < snapshot.Count; i++)
            {
                if (i > 0)
                    sb.Append(',');

                LogEntry entry = snapshot[i];
                sb.Append('{');
                sb.Append("\"index\":").Append(entry.Index.ToString(CultureInfo.InvariantCulture)).Append(',');
                sb.Append("\"time\":").Append(JsonString(entry.Time)).Append(',');
                sb.Append("\"type\":").Append(JsonString(entry.Type)).Append(',');
                sb.Append("\"message\":").Append(JsonString(entry.Message)).Append(',');
                sb.Append("\"stackTrace\":").Append(JsonString(entry.StackTrace));
                sb.Append('}');
            }

            sb.Append("]}");
            return BridgeResponse.Json(200, sb.ToString());
        }

        private static BridgeResponse GetSceneRoots()
        {
            Scene scene = SceneManager.GetActiveScene();
            GameObject[] roots = scene.IsValid() ? scene.GetRootGameObjects() : Array.Empty<GameObject>();

            StringBuilder sb = new StringBuilder();
            sb.Append("{\"ok\":true,\"scene\":").Append(JsonString(scene.IsValid() ? scene.name : string.Empty));
            sb.Append(",\"roots\":[");

            for (int i = 0; i < roots.Length; i++)
            {
                if (i > 0)
                    sb.Append(',');

                AppendGameObjectSummary(sb, roots[i]);
            }

            sb.Append("]}");
            return BridgeResponse.Json(200, sb.ToString());
        }

        private static BridgeResponse GetSceneObjects(Dictionary<string, string> query)
        {
            string filter = GetString(query, "query", string.Empty);
            int limit = GetInt(query, "limit", 120, 1, 500);
            string lowerFilter = filter.ToLowerInvariant();

            Transform[] transforms = UnityEngine.Object.FindObjectsByType<Transform>(FindObjectsInactive.Include, FindObjectsSortMode.None);
            StringBuilder sb = new StringBuilder();
            sb.Append("{\"ok\":true,\"objects\":[");

            int written = 0;
            foreach (Transform transform in transforms)
            {
                if (transform == null || transform.gameObject == null)
                    continue;
                if (EditorUtility.IsPersistent(transform.gameObject))
                    continue;
                if (!transform.gameObject.scene.IsValid())
                    continue;

                string path = GetHierarchyPath(transform.gameObject);
                if (!string.IsNullOrEmpty(lowerFilter) &&
                    path.ToLowerInvariant().IndexOf(lowerFilter, StringComparison.Ordinal) < 0)
                    continue;

                if (written > 0)
                    sb.Append(',');

                AppendGameObjectSummary(sb, transform.gameObject);
                written++;

                if (written >= limit)
                    break;
            }

            sb.Append("],\"count\":").Append(written.ToString(CultureInfo.InvariantCulture)).Append('}');
            return BridgeResponse.Json(200, sb.ToString());
        }

        private static BridgeResponse GetObjectDetails(Dictionary<string, string> query)
        {
            string path = GetString(query, "path", string.Empty);
            GameObject go = FindGameObjectByPath(path);

            if (go == null)
                return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");

            StringBuilder sb = new StringBuilder();
            sb.Append("{\"ok\":true,\"object\":");
            AppendGameObjectDetails(sb, go);
            sb.Append('}');

            return BridgeResponse.Json(200, sb.ToString());
        }

        private static BridgeResponse SetPlayState(bool play, bool pause)
        {
            EditorApplication.isPlaying = play;
            EditorApplication.isPaused = pause;
            return GetHealth();
        }

        private static BridgeResponse SetTransform(string body)
        {
            string path = ExtractString(body, "path");
            if (string.IsNullOrEmpty(path))
                return BridgeResponse.Json(400, "{\"ok\":false,\"error\":\"missing_path\"}");

            GameObject go = FindGameObjectByPath(path);
            if (go == null)
                return BridgeResponse.Json(404, "{\"ok\":false,\"error\":\"object_not_found\",\"path\":" + JsonString(path) + "}");

            Undo.RecordObject(go.transform, "Codex Bridge Set Transform");

            if (TryExtractVector3(body, "position", out Vector3 position))
                go.transform.position = position;
            if (TryExtractVector3(body, "localPosition", out Vector3 localPosition))
                go.transform.localPosition = localPosition;
            if (TryExtractVector3(body, "rotationEuler", out Vector3 rotationEuler))
                go.transform.eulerAngles = rotationEuler;
            if (TryExtractVector3(body, "localRotationEuler", out Vector3 localRotationEuler))
                go.transform.localEulerAngles = localRotationEuler;
            if (TryExtractVector3(body, "scale", out Vector3 scale))
                go.transform.localScale = scale;

            EditorUtility.SetDirty(go.transform);

            StringBuilder sb = new StringBuilder();
            sb.Append("{\"ok\":true,\"object\":");
            AppendGameObjectDetails(sb, go);
            sb.Append('}');

            return BridgeResponse.Json(200, sb.ToString());
        }

        private static void AppendGameObjectSummary(StringBuilder sb, GameObject go)
        {
            sb.Append('{');
            sb.Append("\"name\":").Append(JsonString(go.name)).Append(',');
            sb.Append("\"path\":").Append(JsonString(GetHierarchyPath(go))).Append(',');
            sb.Append("\"activeSelf\":").Append(Bool(go.activeSelf)).Append(',');
            sb.Append("\"activeInHierarchy\":").Append(Bool(go.activeInHierarchy)).Append(',');
            sb.Append("\"scene\":").Append(JsonString(go.scene.IsValid() ? go.scene.name : string.Empty)).Append(',');
            sb.Append("\"position\":").Append(VectorJson(go.transform.position)).Append(',');
            sb.Append("\"rotationEuler\":").Append(VectorJson(go.transform.eulerAngles)).Append(',');
            sb.Append("\"scale\":").Append(VectorJson(go.transform.localScale));
            sb.Append('}');
        }

        private static void AppendGameObjectDetails(StringBuilder sb, GameObject go)
        {
            AppendGameObjectSummary(sb, go);
            sb.Length -= 1;

            Component[] components = go.GetComponents<Component>();
            sb.Append(",\"components\":[");
            for (int i = 0; i < components.Length; i++)
            {
                if (i > 0)
                    sb.Append(',');

                Component component = components[i];
                sb.Append(JsonString(component != null ? component.GetType().FullName : "Missing Script"));
            }
            sb.Append("]}");
        }

        private static GameObject FindGameObjectByPath(string path)
        {
            if (string.IsNullOrEmpty(path))
                return null;

            string[] parts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length == 0)
                return null;

            Scene scene = SceneManager.GetActiveScene();
            if (!scene.IsValid())
                return null;

            foreach (GameObject root in scene.GetRootGameObjects())
            {
                if (root.name != parts[0])
                    continue;

                Transform current = root.transform;
                for (int i = 1; i < parts.Length && current != null; i++)
                {
                    current = FindChildByName(current, parts[i]);
                }

                if (current != null)
                    return current.gameObject;
            }

            return null;
        }

        private static Transform FindChildByName(Transform parent, string childName)
        {
            for (int i = 0; i < parent.childCount; i++)
            {
                Transform child = parent.GetChild(i);
                if (child.name == childName)
                    return child;
            }

            return null;
        }

        private static string GetHierarchyPath(GameObject go)
        {
            Stack<string> parts = new Stack<string>();
            Transform current = go.transform;

            while (current != null)
            {
                parts.Push(current.name);
                current = current.parent;
            }

            return string.Join("/", parts.ToArray());
        }

        private static void OnLogMessageReceived(string condition, string stackTrace, LogType type)
        {
            lock (LogLock)
            {
                Logs.Add(new LogEntry
                {
                    Index = ++_logIndex,
                    Time = DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
                    Type = type.ToString(),
                    Message = condition ?? string.Empty,
                    StackTrace = stackTrace ?? string.Empty
                });

                if (Logs.Count > MaxLogs)
                    Logs.RemoveAt(0);
            }
        }

        private static int GetInt(Dictionary<string, string> query, string key, int fallback, int min, int max)
        {
            if (!query.TryGetValue(key, out string value) ||
                !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed))
                return fallback;

            return Mathf.Clamp(parsed, min, max);
        }

        private static string GetString(Dictionary<string, string> query, string key, string fallback)
        {
            return query.TryGetValue(key, out string value) ? value : fallback;
        }

        private static string ExtractString(string json, string key)
        {
            if (string.IsNullOrEmpty(json))
                return string.Empty;

            Match match = Regex.Match(json,
                "\"" + Regex.Escape(key) + "\"\\s*:\\s*\"((?:\\\\.|[^\"])*)\"",
                RegexOptions.CultureInvariant);

            return match.Success ? Regex.Unescape(match.Groups[1].Value) : string.Empty;
        }

        private static bool TryExtractVector3(string json, string key, out Vector3 value)
        {
            value = Vector3.zero;
            if (string.IsNullOrEmpty(json))
                return false;

            Match objectMatch = Regex.Match(json,
                "\"" + Regex.Escape(key) + "\"\\s*:\\s*\\{(?<body>.*?)\\}",
                RegexOptions.Singleline | RegexOptions.CultureInvariant);

            if (!objectMatch.Success)
                return false;

            string objectBody = objectMatch.Groups["body"].Value;
            if (!TryExtractFloat(objectBody, "x", out float x) ||
                !TryExtractFloat(objectBody, "y", out float y) ||
                !TryExtractFloat(objectBody, "z", out float z))
                return false;

            value = new Vector3(x, y, z);
            return true;
        }

        private static bool TryExtractFloat(string json, string key, out float value)
        {
            value = 0f;
            Match match = Regex.Match(json,
                "\"" + Regex.Escape(key) + "\"\\s*:\\s*(?<num>-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?)",
                RegexOptions.CultureInvariant);

            return match.Success &&
                   float.TryParse(match.Groups["num"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
        }

        private static string VectorJson(Vector3 value)
        {
            return "{\"x\":" + FloatJson(value.x) +
                   ",\"y\":" + FloatJson(value.y) +
                   ",\"z\":" + FloatJson(value.z) + "}";
        }

        private static string FloatJson(float value)
        {
            if (float.IsNaN(value) || float.IsInfinity(value))
                return "0";

            return value.ToString("0.#####", CultureInfo.InvariantCulture);
        }

        private static string Bool(bool value)
        {
            return value ? "true" : "false";
        }

        private static string JsonString(string value)
        {
            if (value == null)
                return "null";

            StringBuilder sb = new StringBuilder(value.Length + 2);
            sb.Append('"');
            foreach (char c in value)
            {
                switch (c)
                {
                    case '\\':
                        sb.Append("\\\\");
                        break;
                    case '"':
                        sb.Append("\\\"");
                        break;
                    case '\n':
                        sb.Append("\\n");
                        break;
                    case '\r':
                        sb.Append("\\r");
                        break;
                    case '\t':
                        sb.Append("\\t");
                        break;
                    default:
                        if (c < 32)
                            sb.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
                        else
                            sb.Append(c);
                        break;
                }
            }
            sb.Append('"');
            return sb.ToString();
        }

        private sealed class BridgeRequest
        {
            public readonly string Method;
            public readonly string Path;
            public readonly Dictionary<string, string> Query;
            public readonly string Body;

            public BridgeRequest(string method, string path, Dictionary<string, string> query, string body)
            {
                Method = method;
                Path = path;
                Query = query;
                Body = body ?? string.Empty;
            }
        }

        private sealed class BridgeResponse
        {
            public readonly int StatusCode;
            public readonly string ContentType;
            public readonly string Body;

            private BridgeResponse(int statusCode, string contentType, string body)
            {
                StatusCode = statusCode;
                ContentType = contentType;
                Body = body;
            }

            public static BridgeResponse Json(int statusCode, string body)
            {
                return new BridgeResponse(statusCode, "application/json", body);
            }
        }

        private sealed class BridgeJob
        {
            public readonly BridgeRequest Request;
            public readonly ManualResetEventSlim Done = new ManualResetEventSlim(false);
            public BridgeResponse Response;

            public BridgeJob(BridgeRequest request)
            {
                Request = request;
            }
        }

        private sealed class LogEntry
        {
            public int Index;
            public string Time;
            public string Type;
            public string Message;
            public string StackTrace;
        }
    }
}
