using GeoSus.Client;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using TMPro;
using UnityEngine;
using UnityEngine.UI;


namespace Subsystems{
    [System.Serializable]
    public class BuildingSettings
    {
        public Material ResidentialBuildingsMat;
        public float ResidentialBuildingHeight;
        public Material CommercialBuildingsMat;
        public float CommercialBuildingHeight;
        public Material IndustrialBuildingsMat;
        public float IndustrialBuildingHeight;
        public Material DefaultBuildingMat;
        public float DefaultBuildingHeight;
    }
    [System.Serializable]
    public class PathwaySettings
    {
        public Material FootwayMat;
        public float FootwayWidth;
        public Material PathMat;
        public float PathWidth;
        public Material StepsMat;
        public float StepsWidth;
        public Material CyclewayMat;
        public float CyclewayWidth;
        public Material PedestrianMat;
        public float PedestrianWidth;
        public Material RoadMat;
        public float RoadWidth;
        public Material ServiceMat;
        public float ServiceWidth;
        public Material ResidentialMat;
        public float ResidentialWidth;
        public Material TrackMat;
        public float TrackWidth;
        public Material DefaultMat;
        public float DefaultWidth;
    }
    [System.Serializable]
    public class AreaSettings
    {
        public Material ParkMat;
        public Material GardenMat;
        public Material PlaygroundMat;
        public Material ForestMat;
        public Material GrassMat;
        public Material WaterMat;
        public Material DefaultMat;
    }
    public class GameManager_Map
    {
        private GameClient _gameClient;
        private GameObject _mapCenterPoint;
        private Position _centerPosition;
        private BuildingSettings _buildingSettings;
        private PathwaySettings _pathwaySettings;
        private AreaSettings _areaSettings;
        private const float _metersPerUnit = 1f;

        // ── Layer Y separation (single source of truth for vertical stacking) ───
        // Areas at the bottom, paths above areas, buildings extruded upward from
        // their own base, POIs floating well above everything else. Z-fighting
        // happens when adjacent geometry shares a Y; these constants keep each
        // logical layer at a distinct elevation.
        private const float kAreaBaseY     = 0.10f;
        private const float kPathY         = 0.30f;
        private const float kBuildingBaseY = 0.50f;
        private const float kPoiY          = 2.00f;

        // Render-queue forcing was tried in P3 to disambiguate same-Y geometry
        // but turned out to be the cause of the "blank map in mobile game view,
        // fine in scene view" regression: forcing transparent-class shaders
        // (default queue 3000+) into the Geometry range (2000-2150) breaks
        // their depth-write/blend assumptions on mobile shader paths. The
        // editor's scene view masks it because it uses different render paths
        // and post-process is off there. Queue forcing removed in P8;
        // disambiguation is now via Y-layering + per-area Y-stagger alone,
        // which the depth buffer resolves correctly even on weak mobile GPUs.

        // ── Marker sizing (top-down camera, units = meters) ─────────────────
        // The camera's orthographic size pushes "1 meter" to a small fraction
        // of the screen. Markers need to be visibly larger than buildings'
        // footprints for instant recognition.
        private const float kMarkerHeight  = 8f;     // pillar height
        private const float kMarkerRadius  = 3f;     // pillar radius (cylinder X/Z)
        private const float kMarkerY       = 4f;     // base Y so pillar centers ~mid-height
        private const float kLabelY        = 9f;     // text label sits above pillar top
        private const float kLabelFontSize = 14f;    // 3D text size in world units

        // Runtime marker collections
        private Dictionary<string, GameObject> _taskMarkers   = new Dictionary<string, GameObject>();
        private Dictionary<string, GameObject> _bodyMarkers   = new Dictionary<string, GameObject>();
        private Dictionary<string, GameObject> _playerAvatars = new Dictionary<string, GameObject>();
        private List<GameObject> _sabotageMarkers             = new List<GameObject>();

        public GameManager_Map(GameClient gameClient, GameObject mapCenterPoint, BuildingSettings buildingSettings, PathwaySettings pathwaySettings, AreaSettings areaSettings)
        {
            _gameClient = gameClient;
            _mapCenterPoint = mapCenterPoint;
            _buildingSettings = buildingSettings;
            _pathwaySettings = pathwaySettings;
            _areaSettings = areaSettings;
        }

        public bool IsSceneReady => _mapCenterPoint != null;

        /// <summary>Called from OnSceneLoaded when Client.unity is loaded so the
        /// MapCenterPoint (which lives in Client.unity) can be wired at runtime.</summary>
        public void SetMapCenterPoint(GameObject go) { _mapCenterPoint = go; }
        public void BuildMap()
        {
            if (_mapCenterPoint == null)
            {
                Debug.LogWarning("[Map] BuildMap skipped: MapCenterPoint is not yet bound.");
                return;
            }
            if (_gameClient?.CurrentLobbyState?.MapData == null)
            {
                Debug.LogWarning("[Map] BuildMap skipped: no MapData in CurrentLobbyState.");
                return;
            }

            ClearChildren();
            _centerPosition = _gameClient.CurrentLobbyState.MapData.Center;
            GameObject buildingsRoot = new GameObject("Buildings");
            buildingsRoot.transform.parent = _mapCenterPoint.transform;

            GameObject pathRoot = new GameObject("Pathways");
            pathRoot.transform.parent = _mapCenterPoint.transform;

            GameObject areaRoot = new GameObject("Areas");
            areaRoot.transform.parent = _mapCenterPoint.transform;

            foreach (var building in _gameClient.CurrentLobbyState.MapData.GetBuildings())
            {
                string buildingType = "Unknown";
                try
                {
                    buildingType = _gameClient.CurrentLobbyState.MapData.BuildingTypes[_gameClient.CurrentLobbyState.MapData.GetBuildings().IndexOf(building)];
                }
                catch (Exception ex) { Debug.Log($"Error: {ex.Message}"); }
                building.Name = buildingType;
                GameObject b = BuildBuildingMesh(building);
                b.transform.parent = buildingsRoot.transform;
            }
            foreach (var path in _gameClient.CurrentLobbyState.MapData.GetPathways())
            {
                GameObject p = BuildPathwayMesh(path);
                p.transform.parent = pathRoot.transform;
            }
            foreach (var area in _gameClient.CurrentLobbyState.MapData.GetAreas())
            {
                GameObject a = BuildAreaMesh(area);
                a.transform.parent = areaRoot.transform;
            }

            GameObject poiRoot = new GameObject("POIs");
            poiRoot.transform.parent = _mapCenterPoint.transform;
            int poiCount = 0;
            foreach (var poi in _gameClient.CurrentLobbyState.MapData.GetPOIs())
            {
                GameObject p = BuildPOIMarker(poi);
                if (p != null) { p.transform.parent = poiRoot.transform; poiCount++; }
            }

            // Diagnostic - if the user reports "map missing in game view" but
            // the counts here are non-zero, the bug is camera/culling related,
            // not a build issue.
            int buildings = _gameClient.CurrentLobbyState.MapData.GetBuildings()?.Count ?? 0;
            int paths     = _gameClient.CurrentLobbyState.MapData.GetPathways()?.Count ?? 0;
            int areas     = _gameClient.CurrentLobbyState.MapData.GetAreas()?.Count ?? 0;
            Debug.Log($"[Map] BuildMap done: {buildings} buildings, {paths} paths, " +
                      $"{areas} areas, {poiCount} POIs. MapCenterPoint={_mapCenterPoint.name} " +
                      $"layer={_mapCenterPoint.layer} pos={_mapCenterPoint.transform.position} " +
                      $"scale={_mapCenterPoint.transform.localScale}");
        }

        /// <summary>
        /// Build a tall, brightly-colored pillar for a Point of Interest with
        /// a 3D text label above it (e.g. "FOOD", "SHOP"). The label is laid
        /// flat on the XZ plane facing UP so it reads correctly under the
        /// orthogonal top-down camera.
        /// </summary>
        private GameObject BuildPOIMarker(MapPOI poi)
        {
            if (poi == null) return null;
            var color = ColorForPOI(poi.POIType);
            string label = LabelForPOI(poi.POIType);
            var pos = poi.Location.ToLocalVector3(_centerPosition);
            return CreateMarkerWithLabel($"POI_{poi.POIType}_{poi.Id}", pos, color, label);
        }

        /// <summary>
        /// Shared marker builder: tall colored cylinder pillar + 3D text label
        /// above it. Used by POIs, tasks, bodies, and sabotage stations so
        /// they all share a visual language ("colored pillar with a name").
        /// </summary>
        private GameObject CreateMarkerWithLabel(string name, Vector3 worldPos, Color color, string label)
        {
            var go = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
            go.name = name;

            // Strip the auto-added collider - markers are visual only.
            var col = go.GetComponent<Collider>();
            if (col != null) UnityEngine.Object.Destroy(col);

            go.transform.position = worldPos + Vector3.up * kMarkerY;
            // Cylinder's default unit is 2 tall, 1 wide. Scale Y by half of
            // kMarkerHeight (built-in is 2 units), X/Z by kMarkerRadius.
            go.transform.localScale = new Vector3(kMarkerRadius, kMarkerHeight * 0.5f, kMarkerRadius);

            var mr = go.GetComponent<MeshRenderer>();
            if (mr != null)
            {
                // One .material access -> single clone of the primitive's
                // default mat. Don't touch renderQueue (P3 regression cause).
                var inst = mr.material;
                if (inst != null) inst.color = color;
            }

            // 3D text label - lays flat on top of the pillar facing up.
            // Parented to the marker so it follows position changes.
            var labelGO = new GameObject("Label");
            labelGO.transform.SetParent(go.transform, worldPositionStays: false);
            // Local Y offset: pillar's local scale Y is kMarkerHeight/2, but
            // the cylinder primitive is 2 units tall in local space, so its
            // top is at local +1. Label sits a hair above that.
            labelGO.transform.localPosition = new Vector3(0, 1.05f, 0);
            // Rotate 90 around X so the text quad's normal points +Y (toward
            // the top-down camera). The default TMP forward is +Z.
            labelGO.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
            // Compensate for the cylinder's non-uniform parent scale so the
            // text size in world units matches kLabelFontSize regardless of
            // how the pillar was scaled.
            labelGO.transform.localScale = new Vector3(
                1f / kMarkerRadius,
                1f / (kMarkerHeight * 0.5f),
                1f / kMarkerRadius);

            var tmp = labelGO.AddComponent<TextMeshPro>();
            tmp.text = label;
            tmp.fontSize = kLabelFontSize;
            tmp.color = Color.white;
            tmp.fontStyle = FontStyles.Bold;
            tmp.alignment = TextAlignmentOptions.Center;
            tmp.outlineColor = Color.black;
            tmp.outlineWidth = 0.25f;
            // Reasonable bounds so the text mesh isn't auto-clipped.
            var rt = tmp.rectTransform;
            rt.sizeDelta = new Vector2(20, 4);

            return go;
        }

        private static Color ColorForPOI(MapPOIType type)
        {
            switch (type)
            {
                case MapPOIType.FoodDrink:  return new Color(1.00f, 0.55f, 0.00f); // orange
                case MapPOIType.Shop:       return new Color(0.20f, 0.60f, 1.00f); // blue
                case MapPOIType.Health:     return new Color(0.96f, 0.27f, 0.27f); // red
                case MapPOIType.Transport:  return new Color(0.85f, 0.85f, 0.20f); // yellow
                case MapPOIType.Culture:    return new Color(0.65f, 0.30f, 0.95f); // purple
                case MapPOIType.Landmark:   return new Color(0.95f, 0.85f, 0.40f); // gold
                case MapPOIType.Recreation: return new Color(0.30f, 0.85f, 0.30f); // green
                default:                    return new Color(0.75f, 0.75f, 0.80f); // muted grey
            }
        }

        private static string LabelForPOI(MapPOIType type)
        {
            switch (type)
            {
                case MapPOIType.FoodDrink:  return "FOOD";
                case MapPOIType.Shop:       return "SHOP";
                case MapPOIType.Health:     return "HEALTH";
                case MapPOIType.Transport:  return "TRANSIT";
                case MapPOIType.Culture:    return "CULTURE";
                case MapPOIType.Landmark:   return "LANDMARK";
                case MapPOIType.Recreation: return "PARK";
                default:                    return "POI";
            }
        }
        void ClearChildren()
        {
            List<GameObject> toDestroy = new List<GameObject>();
            foreach (Transform t in _mapCenterPoint.transform)
                toDestroy.Add(t.gameObject);
            foreach (var g in toDestroy)
            {
                UnityEngine.Object.DestroyImmediate(g);
            }
        }
        #region Mesh Building
        GameObject BuildBuildingMesh(MapBuilding b)
        {
            var building = new GameObject($"Building_{b.Name ?? "Unknown"}");

            // Výpočet středu budovy. Lift the base above kPathY so building
            // walls visibly extrude *upward* from above the road/area layer
            // instead of starting at ground (which made them clip into paved
            // areas that share their footprint).
            Vector3 center = CalculatePolygonCenter(b.Outline);
            building.transform.position = center + Vector3.up * kBuildingBaseY;

            // Vytvoření mesh pro budovu
            MeshFilter meshFilter = building.AddComponent<MeshFilter>();
            MeshRenderer meshRenderer = building.AddComponent<MeshRenderer>();

            float height;
            Material mat;
            switch (b.BuildingType.ToLower())
            {
                case "residential":
                    mat = _buildingSettings.ResidentialBuildingsMat;
                    height = _buildingSettings.ResidentialBuildingHeight;
                    break;
                case "commercial":
                    mat = _buildingSettings.CommercialBuildingsMat;
                    height = _buildingSettings.CommercialBuildingHeight;
                    break;
                case "industrial":
                    mat = _buildingSettings.IndustrialBuildingsMat;
                    height = _buildingSettings.IndustrialBuildingHeight;
                    break;
                default:
                    mat = _buildingSettings.DefaultBuildingMat;
                    height = _buildingSettings.DefaultBuildingHeight;
                    break;
            }
            Mesh mesh = CreateExtrudedPolygonMesh(b.Outline, height);
            meshFilter.mesh = mesh;

            //TODO: material by type
            // Použijeme barvu podle typu budovy. Use sharedMaterial to keep
            // the project's Material asset reference - no clone, no leak.
            // Y-position alone disambiguates building geometry from area/path
            // layers; we don't need renderQueue overrides (which broke mobile
            // rendering for transparent-class shaders in P3).
            meshRenderer.sharedMaterial = mat;

            // Přidání collideru pro interakci
            building.AddComponent<MeshCollider>();
            return building;
        }
        GameObject BuildPathwayMesh(MapPathway w)
        {
            var path = new GameObject($"Path_{w.Name ?? "Unknown"}");

            // Použijeme LineRenderer pro jednoduchost
            LineRenderer line = path.AddComponent<LineRenderer>();
            float width;
            Material mat;
            
            switch (w.PathType)
            {
                case PathType.Footway:
                    mat = _pathwaySettings.FootwayMat;
                    width = _pathwaySettings.FootwayWidth;
                    break;
                case PathType.Path:
                    mat = _pathwaySettings.PathMat;
                    width = _pathwaySettings.PathWidth;
                    break;
                case PathType.Steps:
                    mat = _pathwaySettings.StepsMat;
                    width = _pathwaySettings.PathWidth;
                    break;
                case PathType.Cycleway:
                    mat = _pathwaySettings.CyclewayMat;
                    width = _pathwaySettings.CyclewayWidth;
                    break;
                case PathType.Pedestrian:
                    mat = _pathwaySettings.PedestrianMat;
                    width = _pathwaySettings.PedestrianWidth;
                    break;
                case PathType.Road:
                    mat = _pathwaySettings.RoadMat;
                    width = _pathwaySettings.RoadWidth;
                    break;
                case PathType.Service:
                    mat = _pathwaySettings.ServiceMat;
                    width = _pathwaySettings.ServiceWidth;
                    break;
                case PathType.Residential:
                    mat = _pathwaySettings.ResidentialMat;
                    width = _pathwaySettings.ResidentialWidth;
                    break;
                case PathType.Track:
                    mat = _pathwaySettings.TrackMat;
                    width = _pathwaySettings.TrackWidth;
                    break;
                default:
                    mat = _pathwaySettings.DefaultMat;
                    width = _pathwaySettings.DefaultWidth;
                    break;
            }

            // sharedMaterial avoids the LineRenderer cloning the project's
            // shared path Material on every BuildMap call. Queue overrides
            // dropped (P3 mobile-render regression cause).
            line.sharedMaterial = mat;
            line.widthMultiplier = width;

            // Nastavení bodů cesty - kPathY sits above all area polygons but
            // below building bases, so paths visibly run on top of areas.
            line.positionCount = w.Points.Count;
            for (int i = 0; i < w.Points.Count; i++)
            {
                Vector3 pos = w.Points[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center);
                pos.y = kPathY;
                line.SetPosition(i, pos);
            }
            return path;
        }
        GameObject BuildAreaMesh(MapArea a)
        {
            var area = new GameObject($"Area_{a.Name ?? "Unknown"}");

            MeshFilter meshFilter = area.AddComponent<MeshFilter>();
            MeshRenderer meshRenderer = area.AddComponent<MeshRenderer>();

            // Vytvoření plochého mesh
            Mesh mesh = CreateFlatPolygonMesh(a.Outline);
            meshFilter.mesh = mesh;

            
            Material mat;
            switch (a.AreaType)
            {
                case MapAreaType.Park:
                    mat = _areaSettings.ParkMat;
                    break;
                case MapAreaType.Garden:
                    mat = _areaSettings.GardenMat;
                    break;
                case MapAreaType.Playground:
                    mat = _areaSettings.PlaygroundMat;
                    break;
                case MapAreaType.Forest:
                    mat = _areaSettings.ForestMat;
                    break;
                case MapAreaType.Grass:
                    mat = _areaSettings.GrassMat;
                    break;
                case MapAreaType.Water:
                    mat = _areaSettings.WaterMat;
                    break;
                default:
                    mat = _areaSettings.DefaultMat;
                    break;
            }
            
            // sharedMaterial: no per-area material clone. Render-queue forcing
            // dropped in P8 (caused mobile-render regression). The Y-stagger
            // below alone now drives "smaller polygon on top of larger one"
            // depth ordering - which is what the depth buffer was always
            // designed to do, and works on mobile GPUs with weak precision
            // because the stagger spread (0.04 units) is well above any
            // reasonable depth-buffer epsilon.
            meshRenderer.sharedMaterial = mat;

            // Y stagger: smaller polygons sit a hair higher than larger ones,
            // so depth-test draws them on top of bigger area polygons they sit
            // inside (e.g. a playground inside a park). Total spread is 0.04
            // units - visually invisible but plenty for the depth buffer.
            float yStagger = ComputeAreaYStagger(a.Outline);
            area.transform.position = new Vector3(0, kAreaBaseY + yStagger, 0);

            return area;
        }

        /// <summary>
        /// Returns a non-negative size proxy used to bucket areas by footprint.
        /// Larger polygons return higher numbers; used inversely for queue/Y.
        /// </summary>
        private float AreaSizeBucket(List<Position> outline)
        {
            if (outline == null || outline.Count < 3) return 1f;
            // Cheap bbox area in lat-lon space scaled by 1e6 - we only need a
            // monotonic ordering, not a real geographic area.
            double minLat = outline[0].Lat, maxLat = outline[0].Lat;
            double minLon = outline[0].Lon, maxLon = outline[0].Lon;
            for (int i = 1; i < outline.Count; i++)
            {
                if (outline[i].Lat < minLat) minLat = outline[i].Lat;
                if (outline[i].Lat > maxLat) maxLat = outline[i].Lat;
                if (outline[i].Lon < minLon) minLon = outline[i].Lon;
                if (outline[i].Lon > maxLon) maxLon = outline[i].Lon;
            }
            double bbox = (maxLat - minLat) * (maxLon - minLon) * 1e6;
            return (float)System.Math.Max(0.001, bbox);
        }

        /// <summary>
        /// Smaller areas get a higher Y so they render on top of any larger
        /// area they overlap. Returns a value in [0, 0.04] units.
        /// </summary>
        private float ComputeAreaYStagger(List<Position> outline)
        {
            float bucket = AreaSizeBucket(outline);
            // Inverse mapping: huge area -> 0, tiny area -> 0.04.
            float t = Mathf.Clamp01(1f - bucket / (bucket + 50f));
            return t * 0.04f;
        }
        #endregion
        #region Polygon Utils
        private Vector3 CalculatePolygonCenter(List<Position> points)
        {
            Vector3 center = Vector3.zero;
            foreach (var point in points)
            {
                center += point.ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center);
            }
            return center / points.Count;
        }

        /// <summary>
        /// Signed XZ shoelace area for a polygon expressed in local Vector3.
        /// Positive = CCW (Unity left-handed Y-up: upward-facing normal),
        /// negative = CW (downward-facing normal -> top face invisible from
        /// above unless we reverse the winding before triangulating).
        /// </summary>
        private static float PolygonSignedAreaXZ(List<Vector3> verts)
        {
            float area = 0f;
            int n = verts.Count;
            for (int i = 0; i < n; i++)
            {
                var a = verts[i];
                var b = verts[(i + 1) % n];
                area += (b.x - a.x) * (a.z + b.z);
            }
            return area * 0.5f;
        }
        private Mesh CreateExtrudedPolygonMesh(List<Position> outline, float height)
        {
            Mesh mesh = new Mesh();

            // Reject degenerates - Recast/Overpass can hand back 1-2 vertex
            // outlines on broken ways. Empty mesh -> renderer draws nothing,
            // safer than a malformed triangle list.
            if (outline == null || outline.Count < 3) return mesh;

            // Convert to local space first so we can run a winding check, then
            // reverse if needed. Without this, CW outlines from Overpass yield
            // downward-facing top normals and the building roof is invisible
            // from the top-down map camera.
            int vertexCount = outline.Count;
            var localVerts = new List<Vector3>(vertexCount);
            Vector3 center = CalculatePolygonCenter(outline);
            for (int i = 0; i < vertexCount; i++)
                localVerts.Add(outline[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center) - center);

            if (PolygonSignedAreaXZ(localVerts) < 0f)
                localVerts.Reverse();

            // Vertices - spodní a horní podstava
            Vector3[] vertices = new Vector3[vertexCount * 2];
            for (int i = 0; i < vertexCount; i++)
            {
                Vector3 pos = localVerts[i];
                vertices[i] = pos; // Spodní
                vertices[i + vertexCount] = pos + Vector3.up * height; // Horní
            }

            // Triangles - jen boční stěny pro jednoduchost
            List<int> triangles = new List<int>();

            for (int i = 0; i < vertexCount; i++)
            {
                int next = (i + 1) % vertexCount;

                // Boční stěna - dva trojúhelníky
                triangles.Add(i);
                triangles.Add(i + vertexCount);
                triangles.Add(next);

                triangles.Add(next);
                triangles.Add(i + vertexCount);
                triangles.Add(next + vertexCount);
            }

            // Horní podstava - zjednodušená triangulace (fan)
            if (vertexCount >= 3)
            {
                for (int i = 1; i < vertexCount - 1; i++)
                {
                    triangles.Add(vertexCount); // Střed (první bod horní)
                    triangles.Add(vertexCount + i);
                    triangles.Add(vertexCount + i + 1);
                }
            }

            mesh.vertices = vertices;
            mesh.triangles = triangles.ToArray();
            mesh.RecalculateNormals();
            mesh.RecalculateBounds();

            return mesh;
        }
        private Mesh CreateFlatPolygonMesh(List<Position> outline)
        {
            Mesh mesh = new Mesh();

            // Reject degenerates (matches CreateExtrudedPolygonMesh).
            if (outline == null || outline.Count < 3) return mesh;

            int vertexCount = outline.Count;
            var localVerts = new List<Vector3>(vertexCount);
            Vector3 center = CalculatePolygonCenter(outline);
            for (int i = 0; i < vertexCount; i++)
                localVerts.Add(outline[i].ToLocalVector3(_gameClient.CurrentLobbyState.MapData.Center) - center);

            // Force CCW so RecalculateNormals produces an upward-facing normal.
            // CW polygons from Overpass would otherwise render as black voids
            // when the top-down camera looks at their back face.
            if (PolygonSignedAreaXZ(localVerts) < 0f)
                localVerts.Reverse();

            Vector3[] vertices = localVerts.ToArray();

            // Triangulace - fan pattern
            List<int> triangles = new List<int>();
            for (int i = 1; i < vertexCount - 1; i++)
            {
                triangles.Add(0);
                triangles.Add(i);
                triangles.Add(i + 1);
            }

            mesh.vertices = vertices;
            mesh.triangles = triangles.ToArray();
            mesh.RecalculateNormals();

            return mesh;
        }
        #endregion
        #region Markers

        public void CreateTaskMarkers(List<GeoSus.Client.GameTask> tasks)
        {
            if (_mapCenterPoint == null) return;
            if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0)
            {
                var md = _gameClient?.CurrentLobbyState?.MapData;
                if (md != null) _centerPosition = md.Center;
            }
            if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0) return;
            var taskColor = new Color(0.20f, 0.95f, 0.55f); // bright green - "GO HERE"
            foreach (var task in tasks)
            {
                if (_taskMarkers.ContainsKey(task.TaskId)) continue;
                var pos = task.Location.ToLocalVector3(_centerPosition);
                var go = CreateMarkerWithLabel($"Task_{task.TaskId}", pos, taskColor, "TASK");
                go.transform.parent = _mapCenterPoint.transform;

                // Pulsing point light so the task literally glows on the map.
                var light = go.AddComponent<Light>();
                light.color = taskColor;
                light.intensity = 3f;
                light.range = 25f;
                _taskMarkers[task.TaskId] = go;
            }
        }

        public void RemoveTaskMarker(string taskId)
        {
            if (_taskMarkers.TryGetValue(taskId, out var go))
            {
                UnityEngine.Object.Destroy(go);
                _taskMarkers.Remove(taskId);
            }
        }

        public void CreateBodyMarker(string bodyId, Position location)
        {
            if (_mapCenterPoint == null) return;
            if (_bodyMarkers.ContainsKey(bodyId)) return;
            var pos = location.ToLocalVector3(_centerPosition);
            // Bright red pillar with "BODY" label - players need to see this
            // from across the map to call it in.
            var go = CreateMarkerWithLabel($"Body_{bodyId}", pos,
                new Color(0.96f, 0.18f, 0.18f), "BODY");
            go.transform.parent = _mapCenterPoint?.transform;
            _bodyMarkers[bodyId] = go;
        }

        public void ClearBodyMarkers()
        {
            foreach (var go in _bodyMarkers.Values)
                if (go) UnityEngine.Object.Destroy(go);
            _bodyMarkers.Clear();
        }

        // ── Player avatar sizing ────────────────────────────────────────────
        // The default Unity capsule primitive is 2m tall in local space. The
        // map camera defaults to 150m orthographic-ish height (see
        // MapCameraController), so anything smaller than ~3m world-size is a
        // pixel on screen. Original code used scale=0.4 (~0.8m capsule) which
        // was invisible. Markers (POIs/tasks/bodies) are 8m pillars; players
        // need to be visibly distinct from those AND from each other. The
        // local player gets a halo light + larger scale so the user can find
        // themselves on the map at a glance.
        private const float kLocalPlayerScale  = 4f;   // ~8m capsule (matches marker height)
        private const float kRemotePlayerScale = 2f;   // ~4m capsule (smaller than markers)
        private const float kLocalPlayerHaloRange = 18f;
        private const float kLocalPlayerHaloIntensity = 2.5f;

        public void UpdatePlayerAvatars(Dictionary<string, PlayerPositionInfo> positions, string myUuid)
        {
            if (_mapCenterPoint == null) return;
            if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0)
            {
                var md = _gameClient?.CurrentLobbyState?.MapData;
                if (md != null) _centerPosition = md.Center;
            }
            if (_centerPosition.Lat == 0 && _centerPosition.Lon == 0) return;
            foreach (var kvp in positions)
            {
                string uuid = kvp.Key;
                var info = kvp.Value;
                bool isLocal = uuid == myUuid;
                if (!_playerAvatars.TryGetValue(uuid, out var go) || go == null)
                {
                    go = GameObject.CreatePrimitive(PrimitiveType.Capsule);
                    go.name = $"Player_{uuid.Substring(0, Mathf.Min(8, uuid.Length))}";
                    go.transform.parent = _mapCenterPoint?.transform;
                    // Strip the auto-collider - avatars are visual only and the
                    // collider would interact with the map's MeshColliders.
                    var col = go.GetComponent<Collider>();
                    if (col != null) UnityEngine.Object.Destroy(col);

                    float scale = isLocal ? kLocalPlayerScale : kRemotePlayerScale;
                    go.transform.localScale = Vector3.one * scale;

                    if (isLocal)
                    {
                        // Halo light around the local player so the user can
                        // find themselves at a glance even at the widest zoom.
                        // Range/intensity tuned so it reads as "this is me"
                        // without bleeding far enough to drown POI markers.
                        var halo = go.AddComponent<Light>();
                        halo.color = new Color(0.30f, 1.00f, 0.55f); // matches green capsule color
                        halo.intensity = kLocalPlayerHaloIntensity;
                        halo.range = kLocalPlayerHaloRange;
                    }

                    _playerAvatars[uuid] = go;
                }

                // Lift the avatar so the bottom of the capsule sits roughly at
                // ground level despite the larger scale. Capsule's local pivot
                // is at center, height = 2 * localScale.y world units, so we
                // raise by half the local height.
                float halfHeight = (isLocal ? kLocalPlayerScale : kRemotePlayerScale);
                go.transform.position = info.Position.ToLocalVector3(_centerPosition)
                                      + Vector3.up * halfHeight;

                var mr = go.GetComponent<MeshRenderer>();
                if (mr)
                {
                    if (isLocal) mr.material.color = new Color(0.30f, 1.00f, 0.55f);
                    else if (info.State == GeoSus.Client.PlayerState.Dead) mr.material.color = Color.grey;
                    else mr.material.color = Color.white;
                }
            }
        }

        public void CreateSabotageMarkers(List<RepairStationInfo> stations)
        {
            var color = new Color(1.0f, 0.55f, 0.0f); // strong orange = repair urgency
            foreach (var station in stations)
            {
                var pos = station.Location.ToLocalVector3(_centerPosition);
                var go = CreateMarkerWithLabel($"Sabotage_{station.StationId}", pos,
                    color, "REPAIR");
                go.transform.parent = _mapCenterPoint?.transform;

                // Repair stations also pulse light so impostors and crew see
                // the urgency from across the map.
                var light = go.AddComponent<Light>();
                light.color = color;
                light.intensity = 4f;
                light.range = 30f;
                _sabotageMarkers.Add(go);
            }
        }

        public void ClearSabotageMarkers()
        {
            foreach (var go in _sabotageMarkers)
                if (go) UnityEngine.Object.Destroy(go);
            _sabotageMarkers.Clear();
        }

        #endregion
    }
}
