using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Xml;
using UnityEngine;
using UnityEngine.Networking;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class MapRenderer : MonoBehaviour
{
    [Header("Overpass settings")]
    public const string overpassUrl = "https://mapz.honzuvkod.dev/api/interpreter";
    public float queryRadiusMeters = 200f; // radius around lat/lon to query

    [Header("Location (lat, lon)")]
    public GPSManager gpsManager;
    private double latitude = 50.7727878;
    private double longitude = 15.0718625;

    [Header("Building settings")]
    public Material buildingMaterial;
    public float defaultFloorHeight = 3.0f; // meters per level
    public float defaultBuildingHeight = 6.0f; // if no tags

    [Header("Road settings")]
    public Material roadMaterial;
    public float defaultRoadWidth = 4.0f; // meters
    public float motorwayWidth = 10.0f; 
    public float primaryWidth = 8.0f; 
    public float secondaryWidth = 6.0f; 
    public float tertiaryWidth = 5.0f; 


    [Header("Misc")]
    public float _metersPerUnit = 1f; // scale: 1 unit = 1 meter


    [Header("Storage")]
    Dictionary<long, Vector2> nodes = new Dictionary<long, Vector2>(); // id -> latlon
    List<Way> parsedWays = new List<Way>();

    void Start()
    {
        StartCoroutine(RenderMap());
    }
    IEnumerator RenderMap()
    {
        ClearChildren();

        double[] GPS = gpsManager.GetLastCoords();
        latitude = GPS[0];
        longitude = GPS[1];

        string q = $"[out:xml][timeout:90];(way[\"building\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")});way[\"highway\"](around:{queryRadiusMeters.ToString().Replace(",", ".")},{latitude.ToString().Replace(",", ".")},{longitude.ToString().Replace(",", ".")}););(._;>;);out body;";

        WWWForm form = new WWWForm();
        form.AddField("data", q);

        using (UnityWebRequest www = UnityWebRequest.Post(overpassUrl, form))
        {
            www.downloadHandler = new DownloadHandlerBuffer();
            yield return www.SendWebRequest();

            if (www.result != UnityWebRequest.Result.Success)
            {
                Debug.LogError("Overpass request failed: " + www.error);
                yield break;
            }

            string xml = www.downloadHandler.text;
            ParseOverpassXml(xml);

            GameObject buildingsRoot = new GameObject("Buildings");
            buildingsRoot.transform.parent = this.transform;

            GameObject roadsRoot = new GameObject("Roads");
            roadsRoot.transform.parent = this.transform;

            foreach (var w in parsedWays)
            {
                if (w.tags.ContainsKey("building"))
                {
                    GameObject b = BuildBuildingMesh(w);
                    b.transform.parent = buildingsRoot.transform;
                }
                else if (w.tags.ContainsKey("highway"))
                {
                    GameObject r = BuildRoadMesh(w);
                    r.transform.parent = roadsRoot.transform;
                }
            }

            Debug.Log("Map generation complete: " + parsedWays.Count + " ways, " + nodes.Count + " nodes.");
        }
        yield return StartCoroutine(RenderMap());
    }

    void ClearChildren()
    {
        List<GameObject> toDestroy = new List<GameObject>();
        foreach (Transform t in transform)
            toDestroy.Add(t.gameObject);
        foreach (var g in toDestroy)
            DestroyImmediate(g);
    }

    #region Overpass XML parsing
    class Way
    {
        public long id;
        public List<long> nodeRefs = new List<long>();
        public Dictionary<string, string> tags = new Dictionary<string, string>();
    }



    void ParseOverpassXml(string xmlText)
    {
        nodes.Clear();
        parsedWays.Clear();

        XmlDocument doc = new XmlDocument();
        doc.LoadXml(xmlText);

        XmlNode osm = doc.SelectSingleNode("/osm");
        if (osm == null) return;

        // parse nodes
        foreach (XmlNode node in osm.SelectNodes("node"))
        {
            long id = long.Parse(node.Attributes["id"].Value, CultureInfo.InvariantCulture);
            double lat = double.Parse(node.Attributes["lat"].Value, CultureInfo.InvariantCulture);
            double lon = double.Parse(node.Attributes["lon"].Value, CultureInfo.InvariantCulture);
            nodes[id] = new Vector2((float)lat, (float)lon);
        }

        // parse ways
        foreach (XmlNode wayNode in osm.SelectNodes("way"))
        {
            Way w = new Way();
            w.id = long.Parse(wayNode.Attributes["id"].Value, CultureInfo.InvariantCulture);
            foreach (XmlNode child in wayNode.ChildNodes)
            {
                if (child.Name == "nd")
                {
                    long r = long.Parse(child.Attributes["ref"].Value, CultureInfo.InvariantCulture);
                    w.nodeRefs.Add(r);
                }
                else if (child.Name == "tag")
                {
                    string k = child.Attributes["k"].Value;
                    string v = child.Attributes["v"].Value;
                    w.tags[k] = v;
                }
            }
            parsedWays.Add(w);
        }
    }
    #endregion

    #region Utilities: latlon to local meters
    // Convert latitude/longitude to local XY meters relative to center point
    Vector3 LatLonToLocal(double lat, double lon)
    {
        // Use simple equirectangular projection around center (latitude, longitude)
        double lat0 = latitude;
        double lon0 = longitude;
        double dLat = (lat - lat0) * Mathf.Deg2Rad;
        double dLon = (lon - lon0) * Mathf.Deg2Rad;
        double R = 6378137.0; // Earth radius in meters
        double x = R * dLon * Math.Cos(lat0 * Mathf.Deg2Rad);
        double y = R * dLat;
        return new Vector3((float)x / _metersPerUnit, 0f, (float)y / _metersPerUnit);
    }
    Vector3 NodeIdToLocal(long nodeId)
    {
        if (!nodes.ContainsKey(nodeId))
            return Vector3.zero;
        Vector2 latlon = nodes[nodeId];
        return LatLonToLocal(latlon.x, latlon.y);
    }
    #endregion

    #region Mesh builders
    GameObject BuildBuildingMesh(Way w)
    {
        // gather polygon points
        List<Vector3> poly = new List<Vector3>();
        foreach (var id in w.nodeRefs)
        {
            Vector3 p = NodeIdToLocal(id);
            poly.Add(p);
        }

        // ensure closed
        if (poly.Count < 3) return null;
        if ((poly[0] - poly[poly.Count - 1]).sqrMagnitude > 0.0001f)
            poly.Add(poly[0]);

        // determine height
        float height = defaultBuildingHeight;
        if (w.tags.ContainsKey("height"))
        {
            if (TryParseHeight(w.tags["height"], out float h)) height = h;
        }
        else if (w.tags.ContainsKey("building:levels"))
        {
            if (float.TryParse(w.tags["building:levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
                height = Mathf.Max(0.5f, levels * defaultFloorHeight);
        }
        else if (w.tags.ContainsKey("levels"))
        {
            if (float.TryParse(w.tags["levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
                height = Mathf.Max(0.5f, levels * defaultFloorHeight);
        }

        // create GameObject
        GameObject go = new GameObject("Building_" + w.id);
        MeshFilter mf = go.AddComponent<MeshFilter>();
        MeshRenderer mr = go.AddComponent<MeshRenderer>();
        mr.material = buildingMaterial;

        // generate mesh: roof (triangulated polygon) + walls (extruded quads)
        Mesh mesh = new Mesh();
        mesh.name = "BuildingMesh_" + w.id;

        // Convert poly to 2D points (XZ plane)
        List<Vector2> poly2D = new List<Vector2>();
        for (int i = 0; i < poly.Count - 1; i++) // omit last repeated point
            poly2D.Add(new Vector2(poly[i].x, poly[i].z));

        // triangulate roof
        List<int> roofTris = Triangulate(poly2D);
        if (roofTris == null || roofTris.Count == 0)
        {
            Debug.LogWarning("Triangulation failed for building " + w.id);
            return go;
        }

        // Build vertices: roof vertices at y=height, walls vertices (2 per poly vertex)
        int n = poly2D.Count;
        // Build vertices and triangles with NO SHARED VERTICES (flat shading)
        List<Vector3> verts = new List<Vector3>();
        List<int> triangles = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        // Roof triangles - each triangle gets its own vertices
        for (int i = 0; i < roofTris.Count; i += 3)
        {
            int idx0 = roofTris[i];
            int idx1 = roofTris[i + 1];
            int idx2 = roofTris[i + 2];

            Vector2 p0 = poly2D[idx0];
            Vector2 p1 = poly2D[idx1];
            Vector2 p2 = poly2D[idx2];

            int baseIdx = verts.Count;
            verts.Add(new Vector3(p0.x, height / _metersPerUnit, p0.y));
            verts.Add(new Vector3(p1.x, height / _metersPerUnit, p1.y));
            verts.Add(new Vector3(p2.x, height / _metersPerUnit, p2.y));

            triangles.Add(baseIdx);
            triangles.Add(baseIdx + 1);
            triangles.Add(baseIdx + 2);

            uvs.Add(new Vector2(p0.x, p0.y));
            uvs.Add(new Vector2(p1.x, p1.y));
            uvs.Add(new Vector2(p2.x, p2.y));
        }

        // Walls - each quad gets its own 4 vertices
        for (int i = 0; i < n; i++)
        {
            int iNext = (i + 1) % n;
            Vector2 p0 = poly2D[i];
            Vector2 p1 = poly2D[iNext];

            int baseIdx = verts.Count;
            verts.Add(new Vector3(p0.x, height / _metersPerUnit, p0.y)); // top left
            verts.Add(new Vector3(p0.x, 0, p0.y)); // bottom left
            verts.Add(new Vector3(p1.x, 0, p1.y)); // bottom right
            verts.Add(new Vector3(p1.x, height / _metersPerUnit, p1.y)); // top right

            triangles.Add(baseIdx);
            triangles.Add(baseIdx + 1);
            triangles.Add(baseIdx + 2);

            triangles.Add(baseIdx);
            triangles.Add(baseIdx + 2);
            triangles.Add(baseIdx + 3);

            uvs.Add(new Vector2(0, 1));
            uvs.Add(new Vector2(0, 0));
            uvs.Add(new Vector2(1, 0));
            uvs.Add(new Vector2(1, 1));
        }

        mesh.SetVertices(verts);
        mesh.SetTriangles(triangles, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        mf.mesh = mesh;

        // Center object (using first roof vertex as reference)
        Vector3 centroid = Vector3.zero;
        for (int i = 0; i < roofTris.Count; i += 3)
        {
            centroid += verts[i];
        }
        centroid /= (roofTris.Count / 3);
        go.transform.position = centroid * -1f;

        // Move the roof/walls vertices back to local space
        Vector3[] adjustedVerts = mesh.vertices;
        for (int i = 0; i < adjustedVerts.Length; i++) adjustedVerts[i] += centroid;
        mesh.vertices = adjustedVerts;
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        return go;
    }

    GameObject BuildRoadMesh(Way w)
    {
        // build polyline
        List<Vector3> pts = new List<Vector3>();
        foreach (var id in w.nodeRefs)
            pts.Add(NodeIdToLocal(id));
        if (pts.Count < 2) return null;

        float width = defaultRoadWidth;
        if (w.tags.ContainsKey("width") && float.TryParse(w.tags["width"], NumberStyles.Float, CultureInfo.InvariantCulture, out float wv))
            width = wv;
        else if (w.tags.ContainsKey("highway"))
        {
            // simple heuristic
            string h = w.tags["highway"];
            if (h == "motorway") width = motorwayWidth;
            else if (h == "primary") width = primaryWidth;
            else if (h == "secondary") width = secondaryWidth;
            else if (h == "tertiary") width = tertiaryWidth;
            else width = defaultRoadWidth;
        }

        GameObject go = new GameObject("Road_" + w.id);
        MeshFilter mf = go.AddComponent<MeshFilter>();
        MeshRenderer mr = go.AddComponent<MeshRenderer>();
        mr.material = roadMaterial;

        Mesh mesh = new Mesh();
        mesh.name = "RoadMesh_" + w.id;

        List<Vector3> verts = new List<Vector3>();
        List<int> tris = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        // build quad strip
        for (int i = 0; i < pts.Count; i++)
        {
            Vector3 p = pts[i];
            Vector3 dir;
            if (i == 0) dir = (pts[i + 1] - p).normalized;
            else if (i == pts.Count - 1) dir = (p - pts[i - 1]).normalized;
            else dir = (pts[i + 1] - pts[i - 1]).normalized;

            Vector3 normal = Vector3.Cross(dir, Vector3.up).normalized;
            Vector3 left = p + normal * (width * 0.5f / _metersPerUnit);
            Vector3 right = p - normal * (width * 0.5f / _metersPerUnit);
            verts.Add(left);
            verts.Add(right);
            uvs.Add(new Vector2(0, i));
            uvs.Add(new Vector2(1, i));

            if (i > 0)
            {
                int baseIdx = verts.Count - 4;
                tris.Add(baseIdx + 0);
                tris.Add(baseIdx + 2);
                tris.Add(baseIdx + 1);

                tris.Add(baseIdx + 1);
                tris.Add(baseIdx + 2);
                tris.Add(baseIdx + 3);
            }
        }

        mesh.SetVertices(verts);
        mesh.SetTriangles(tris, 0);
        mesh.SetUVs(0, uvs);
        mesh.RecalculateNormals();
        mesh.RecalculateBounds();

        mf.mesh = mesh;
        go.transform.position = Vector3.zero;
        return go;
    }
    #endregion

    #region Helpers
    bool TryParseHeight(string s, out float meters)
    {
        // try to parse heights like "12", "12.5m", "40 ft"
        s = s.Trim();
        meters = 0f;
        if (s.EndsWith("m")) s = s.Substring(0, s.Length - 1).Trim();
        if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
        {
            meters = v;
            return true;
        }
        // fallback: try to extract number
        StringBuilder num = new StringBuilder();
        foreach (char c in s)
            if ((c >= '0' && c <= '9') || c == '.' || c == ',') num.Append(c == ',' ? '.' : c);
        if (num.Length > 0 && float.TryParse(num.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out v))
        {
            meters = v; return true;
        }
        return false;
    }

    // Basic ear clipping triangulation for simple polygons (2D)
    List<int> Triangulate(List<Vector2> poly)
    {
        List<int> indices = new List<int>();
        int n = poly.Count;
        if (n < 3) return indices;

        List<int> V = new List<int>();
        for (int i = 0; i < n; i++) V.Add(i);

        int guard = 0;
        while (V.Count > 3 && guard < 10000)
        {
            bool earFound = false;
            for (int i = 0; i < V.Count; i++)
            {
                int prev = V[(i - 1 + V.Count) % V.Count];
                int curr = V[i];
                int next = V[(i + 1) % V.Count];

                Vector2 a = poly[prev];
                Vector2 b = poly[curr];
                Vector2 c = poly[next];

                if (!IsConvex(a, b, c)) continue;

                bool hasPointInside = false;
                for (int j = 0; j < V.Count; j++)
                {
                    int vi = V[j];
                    if (vi == prev || vi == curr || vi == next) continue;
                    if (PointInTriangle(poly[vi], a, b, c)) { hasPointInside = true; break; }
                }
                if (hasPointInside) continue;

                // ear found
                indices.Add(prev);
                indices.Add(curr);
                indices.Add(next);
                V.RemoveAt(i);
                earFound = true;
                break;
            }
            if (!earFound) break;
            guard++;
        }

        if (V.Count == 3)
        {
            indices.Add(V[0]); indices.Add(V[1]); indices.Add(V[2]);
        }

        return indices;
    }

    bool IsConvex(Vector2 a, Vector2 b, Vector2 c)
    {
        return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0f; // changed > to 
    }
    bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c)
    {
        float area = TriangleArea(a, b, c);
        float area1 = TriangleArea(p, b, c);
        float area2 = TriangleArea(a, p, c);
        float area3 = TriangleArea(a, b, p);
        return Mathf.Abs(area - (area1 + area2 + area3)) < 1e-3f;
    }

    float TriangleArea(Vector2 a, Vector2 b, Vector2 c)
    {
        return Mathf.Abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) * 0.5f);
    }
    #endregion
}
