// Assets/Editor/GeoTileImporter.cs
// Robust terrain tile importer for Unity (URP or Built-in).
// - Parses CSV header -> column name mapping (order-independent)
// - Validates required columns exist
// - Imports 16-bit PNG heightmaps (tries Single Channel R16 + ushort pixel read; falls back safely)

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEditor;
using UnityEngine;

public class GeoTileImporter : EditorWindow
{
    private string tilesCsvPath = "Assets/GeoData/tile_index.csv";
    private string heightmapsDir = "Assets/GeoData/height_png16";
    private string orthoDir = "Assets/GeoData/ortho_jpg";
    private string buildingsDir = "Assets/GeoData/buildings_tiles";
    private string buildingsEnhancedDir = "Assets/GeoData/buildings_enhanced";
    private string treesDir = "Assets/GeoData/trees_tiles";
    private string treeProxyPath = "Assets/GeoData/tree_proxies.glb";
    private string furnitureDir = "Assets/GeoData/street_furniture";
    private string enhancedTreesDir = "Assets/GeoData/trees_enhanced";

    private float tileSizeMeters = 1000f;
    private int heightmapResolution = 1025;

    private string parentName = "Geo_Terrain_Tiles";
    private string buildingsParentName = "Geo_Buildings";
    private string treesParentName = "Geo_Trees";
    private string furnitureParentName = "Geo_Furniture";
    private bool deleteExisting = false;
    private bool applyOrthoTextures = true;
    private bool importBuildings = true;
    private bool useEnhancedBuildings = false;
    private bool importTrees = true;
    private bool importFurniture = false;
    private bool deleteExistingBuildings = false;
    private bool deleteExistingTrees = false;
    private bool deleteExistingFurniture = false;
    private bool importEnhancedTrees = false;
    private bool deleteExistingEnhancedTrees = false;
    private string enhancedTreesParentName = "Geo_Trees_Enhanced";

    // Prefabs for trees and furniture (assign in editor)
    private GameObject treePrefab;
    private GameObject lampPrefab;
    private GameObject benchPrefab;
    private GameObject signPrefab;
    private GameObject bollardPrefab;
    private GameObject defaultFurniturePrefab; // Fallback for unknown types

    [MenuItem("Tools/Geo Tiles/Import Terrain Tiles (PNG16)")]
    public static void ShowWindow()
    {
        var win = GetWindow<GeoTileImporter>("Geo Tile Importer");
        win.minSize = new Vector2(620, 300);
    }

    private void OnGUI()
    {
        GUILayout.Label("Inputs", EditorStyles.boldLabel);
        tilesCsvPath = EditorGUILayout.TextField("tile_index.csv", tilesCsvPath);
        heightmapsDir = EditorGUILayout.TextField("height_png16 dir", heightmapsDir);
        orthoDir = EditorGUILayout.TextField("ortho_jpg dir", orthoDir);
        buildingsDir = EditorGUILayout.TextField("buildings_glb dir", buildingsDir);
        treesDir = EditorGUILayout.TextField("trees_glb dir", treesDir);
        treeProxyPath = EditorGUILayout.TextField("tree_proxies.glb", treeProxyPath);

        GUILayout.Space(10);
        GUILayout.Label("Terrain", EditorStyles.boldLabel);
        tileSizeMeters = EditorGUILayout.FloatField("Tile size (m)", tileSizeMeters);
        heightmapResolution = EditorGUILayout.IntField("Heightmap resolution", heightmapResolution);

        GUILayout.Space(10);
        GUILayout.Label("Scene", EditorStyles.boldLabel);
        parentName = EditorGUILayout.TextField("Parent object name", parentName);
        deleteExisting = EditorGUILayout.ToggleLeft("Delete existing terrains under parent", deleteExisting);
        applyOrthoTextures = EditorGUILayout.ToggleLeft("Apply ortho texture per tile", applyOrthoTextures);
        buildingsParentName = EditorGUILayout.TextField("Buildings parent name", buildingsParentName);
        buildingsEnhancedDir = EditorGUILayout.TextField("Buildings enhanced dir", buildingsEnhancedDir);
        deleteExistingBuildings = EditorGUILayout.ToggleLeft("Delete existing buildings under parent", deleteExistingBuildings);
        importBuildings = EditorGUILayout.ToggleLeft("Import buildings (GLB per tile)", importBuildings);
        useEnhancedBuildings = EditorGUILayout.ToggleLeft("Use enhanced buildings (from buildings_enhanced/)", useEnhancedBuildings);

        GUILayout.Space(5);
        treesParentName = EditorGUILayout.TextField("Trees parent name", treesParentName);
        deleteExistingTrees = EditorGUILayout.ToggleLeft("Delete existing trees under parent", deleteExistingTrees);
        importTrees = EditorGUILayout.ToggleLeft("Import trees (GLB chunks per tile)", importTrees);

        GUILayout.Space(5);
        furnitureParentName = EditorGUILayout.TextField("Furniture parent name", furnitureParentName);
        furnitureDir = EditorGUILayout.TextField("Furniture CSV dir", furnitureDir);
        deleteExistingFurniture = EditorGUILayout.ToggleLeft("Delete existing furniture under parent", deleteExistingFurniture);
        importFurniture = EditorGUILayout.ToggleLeft("Import street furniture (from CSV)", importFurniture);

        GUILayout.Space(5);
        enhancedTreesParentName = EditorGUILayout.TextField("Enhanced trees parent", enhancedTreesParentName);
        enhancedTreesDir = EditorGUILayout.TextField("Enhanced trees CSV dir", enhancedTreesDir);
        deleteExistingEnhancedTrees = EditorGUILayout.ToggleLeft("Delete existing enhanced trees", deleteExistingEnhancedTrees);
        importEnhancedTrees = EditorGUILayout.ToggleLeft("Import enhanced trees (CSV with canopy colors)", importEnhancedTrees);

        GUILayout.Space(10);
        GUILayout.Label("Prefabs (optional)", EditorStyles.boldLabel);
        treePrefab = (GameObject)EditorGUILayout.ObjectField("Tree Prefab", treePrefab, typeof(GameObject), false);
        lampPrefab = (GameObject)EditorGUILayout.ObjectField("Lamp Prefab", lampPrefab, typeof(GameObject), false);
        benchPrefab = (GameObject)EditorGUILayout.ObjectField("Bench Prefab", benchPrefab, typeof(GameObject), false);
        signPrefab = (GameObject)EditorGUILayout.ObjectField("Sign Prefab", signPrefab, typeof(GameObject), false);
        bollardPrefab = (GameObject)EditorGUILayout.ObjectField("Bollard Prefab", bollardPrefab, typeof(GameObject), false);
        defaultFurniturePrefab = (GameObject)EditorGUILayout.ObjectField("Default Furniture Prefab", defaultFurniturePrefab, typeof(GameObject), false);

        GUILayout.Space(12);
        if (GUILayout.Button("Import / Rebuild"))
            ImportTiles();

        EditorGUILayout.HelpBox(
            "Creates one Unity Terrain per CSV row and positions tiles on a meter grid.\n" +
            "Absolute elevation mapping: Terrain Y = tile_min (or global_min), Terrain height = (tile_max - tile_min).\n" +
            "CSV is header-driven (order-independent). Optionally applies ortho JPGs and instantiates buildings/trees GLBs.",
            MessageType.Info);
    }

    private static void EnsureHeightmapImportSettings(string assetPath)
    {
        var ti = (TextureImporter)AssetImporter.GetAtPath(assetPath);
        if (ti == null) return;

        bool changed = false;

        if (!ti.isReadable) { ti.isReadable = true; changed = true; }
        if (ti.sRGBTexture) { ti.sRGBTexture = false; changed = true; }

        if (ti.textureCompression != TextureImporterCompression.Uncompressed)
        {
            ti.textureCompression = TextureImporterCompression.Uncompressed;
            changed = true;
        }

        if (ti.npotScale != TextureImporterNPOTScale.None)
        {
            ti.npotScale = TextureImporterNPOTScale.None; // keep 1025x1025
            changed = true;
        }

        if (ti.textureType != TextureImporterType.SingleChannel)
        {
            ti.textureType = TextureImporterType.SingleChannel;
            changed = true;
        }

        var ps = ti.GetDefaultPlatformTextureSettings();
        if (ps.format != TextureImporterFormat.R16)
        {
            ps.format = TextureImporterFormat.R16;
            ti.SetPlatformTextureSettings(ps);
            changed = true;
        }

        changed |= EnsurePlatformR16(ti, "Standalone");
        changed |= EnsurePlatformR16(ti, "Android");
        changed |= EnsurePlatformR16(ti, "iPhone");

        if (changed) ti.SaveAndReimport();
    }

    private static bool EnsurePlatformR16(TextureImporter ti, string platform)
    {
        var ps = ti.GetPlatformTextureSettings(platform);

        bool changed = false;
        if (ps.name != platform)
        {
            ps = new TextureImporterPlatformSettings
            {
                name = platform,
                overridden = true,
                format = TextureImporterFormat.R16,
                textureCompression = TextureImporterCompression.Uncompressed,
            };
            ti.SetPlatformTextureSettings(ps);
            return true;
        }

        if (!ps.overridden) { ps.overridden = true; changed = true; }
        if (ps.format != TextureImporterFormat.R16) { ps.format = TextureImporterFormat.R16; changed = true; }
        if (ps.textureCompression != TextureImporterCompression.Uncompressed)
        {
            ps.textureCompression = TextureImporterCompression.Uncompressed;
            changed = true;
        }

        if (changed) ti.SetPlatformTextureSettings(ps);
        return changed;
    }

    private static string NormalizeHeader(string s)
        => (s ?? "").Trim().ToLowerInvariant();

    private static void EnsureOrthoImportSettings(string assetPath)
    {
        var ti = (TextureImporter)AssetImporter.GetAtPath(assetPath);
        if (ti == null) return;

        bool changed = false;
        if (!ti.isReadable) { ti.isReadable = true; changed = true; }
        if (!ti.sRGBTexture) { ti.sRGBTexture = true; changed = true; }
        if (ti.npotScale != TextureImporterNPOTScale.None) { ti.npotScale = TextureImporterNPOTScale.None; changed = true; }
        if (ti.wrapMode != TextureWrapMode.Clamp) { ti.wrapMode = TextureWrapMode.Clamp; changed = true; }
        if (ti.textureCompression != TextureImporterCompression.Uncompressed)
        {
            ti.textureCompression = TextureImporterCompression.Uncompressed;
            changed = true;
        }

        if (changed) ti.SaveAndReimport();
    }

    private static Dictionary<string, int> BuildHeaderMap(string headerLine)
    {
        var map = new Dictionary<string, int>();
        var cols = headerLine.Split(',');
        for (int i = 0; i < cols.Length; i++)
        {
            var key = NormalizeHeader(cols[i]);
            if (string.IsNullOrEmpty(key)) continue;
            if (!map.ContainsKey(key))
                map[key] = i;
        }
        return map;
    }

    private static bool HasAll(Dictionary<string, int> map, params string[] required)
    {
        foreach (var r in required)
            if (!map.ContainsKey(NormalizeHeader(r)))
                return false;
        return true;
    }

    private void ImportTiles()
    {
        Debug.Log($"[GeoTileImporter] START\n  csv={tilesCsvPath}\n  pngDir={heightmapsDir}");

        if (!File.Exists(tilesCsvPath))
        {
            Debug.LogError($"[GeoTileImporter] CSV not found: {tilesCsvPath}");
            return;
        }
        if (!Directory.Exists(heightmapsDir))
        {
            Debug.LogError($"[GeoTileImporter] Heightmap dir not found: {heightmapsDir}");
            return;
        }
        if (applyOrthoTextures && !Directory.Exists(orthoDir))
        {
            Debug.LogWarning($"[GeoTileImporter] Ortho dir not found: {orthoDir} (textures will be skipped).");
            applyOrthoTextures = false;
        }

        var parent = GameObject.Find(parentName);
        if (parent == null) parent = new GameObject(parentName);

        if (deleteExisting)
        {
            for (int i = parent.transform.childCount - 1; i >= 0; i--)
                DestroyImmediate(parent.transform.GetChild(i).gameObject);
        }

        var ci = CultureInfo.InvariantCulture;
        var lines = File.ReadAllLines(tilesCsvPath);

        Debug.Log($"[GeoTileImporter] Read {lines.Length} lines.");
        if (lines.Length < 2)
        {
            Debug.LogError("[GeoTileImporter] CSV has no data rows (need header + at least 1 row).");
            return;
        }

        var headerLine = lines[0].Trim();
        var headerMap = BuildHeaderMap(headerLine);
        Debug.Log($"[GeoTileImporter] Header: {headerLine}");
        Debug.Log($"[GeoTileImporter] Header columns mapped: {string.Join(", ", headerMap.Keys)}");

        // Required columns (order-independent)
        string[] required = { "tile_id", "xmin", "ymin", "global_min", "global_max", "out_res" };
        if (!HasAll(headerMap, required))
        {
            Debug.LogError("[GeoTileImporter] CSV missing required columns. Required: " +
                           string.Join(", ", required) +
                           "\nFound: " + string.Join(", ", headerMap.Keys));
            return;
        }

        int IDX_TILE = headerMap["tile_id"];
        int IDX_XMIN = headerMap["xmin"];
        int IDX_YMIN = headerMap["ymin"];
        int IDX_GMIN = headerMap["global_min"];
        int IDX_GMAX = headerMap["global_max"];
        int IDX_RES = headerMap["out_res"];
        bool hasTileMin = headerMap.TryGetValue("tile_min", out int IDX_TMIN);
        bool hasTileMax = headerMap.TryGetValue("tile_max", out int IDX_TMAX);
        bool useTileRange = hasTileMin && hasTileMax;

        // Compute origin from min xmin/ymin
        double originX = double.PositiveInfinity;
        double originY = double.PositiveInfinity;

        int validRowsForOrigin = 0;
        for (int i = 1; i < lines.Length; i++)
        {
            var line = lines[i].Trim();
            if (string.IsNullOrWhiteSpace(line)) continue;

            var parts = line.Split(',');
            // Robust: just ensure indices exist in this row
            int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
            if (useTileRange)
                needMaxIndex = Math.Max(needMaxIndex, Math.Max(IDX_TMIN, IDX_TMAX));
            if (parts.Length <= needMaxIndex)
            {
                Debug.LogWarning($"[GeoTileImporter] Origin scan: skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
                continue;
            }

            try
            {
                double xmin = double.Parse(parts[IDX_XMIN], ci);
                double ymin = double.Parse(parts[IDX_YMIN], ci);
                originX = Math.Min(originX, xmin);
                originY = Math.Min(originY, ymin);
                validRowsForOrigin++;
            }
            catch (Exception e)
            {
                Debug.LogWarning($"[GeoTileImporter] Origin scan parse failed line {i + 1}: '{line}'\n{e.Message}");
            }
        }

        if (validRowsForOrigin == 0 || double.IsInfinity(originX) || double.IsInfinity(originY))
        {
            Debug.LogError("[GeoTileImporter] Could not compute origin (no valid rows parsed). Check CSV numeric format.");
            return;
        }

        Debug.Log($"[GeoTileImporter] Origin: ({originX}, {originY}) from {validRowsForOrigin} valid rows.");

        int imported = 0, skipped = 0;
        int importedTextures = 0;
        var placements = new List<(string tileId, float ux, float uz, float baseMin)>();

        for (int i = 1; i < lines.Length; i++)
        {
            var line = lines[i].Trim();
            if (string.IsNullOrWhiteSpace(line)) continue;

            var parts = line.Split(',');
            int needMaxIndex = Math.Max(Math.Max(Math.Max(Math.Max(IDX_TILE, IDX_XMIN), IDX_YMIN), IDX_GMIN), Math.Max(IDX_GMAX, IDX_RES));
            if (useTileRange)
                needMaxIndex = Math.Max(needMaxIndex, Math.Max(IDX_TMIN, IDX_TMAX));
            if (parts.Length <= needMaxIndex)
            {
                skipped++;
                Debug.LogWarning($"[GeoTileImporter] Skipping line {i + 1} (too few columns: {parts.Length}). Line: '{line}'");
                continue;
            }

            string tileId = parts[IDX_TILE].Trim();

            double xmin, ymin, gmin, gmax;
            double tileMin = 0.0;
            double tileMax = 0.0;
            bool tileRangeValid = false;
            int outRes;
            try
            {
                xmin = double.Parse(parts[IDX_XMIN], ci);
                ymin = double.Parse(parts[IDX_YMIN], ci);
                gmin = double.Parse(parts[IDX_GMIN], ci);
                gmax = double.Parse(parts[IDX_GMAX], ci);
                outRes = int.Parse(parts[IDX_RES], ci);
            }
            catch (Exception e)
            {
                skipped++;
                Debug.LogWarning($"[GeoTileImporter] Parse failed line {i + 1} tile '{tileId}': {e.Message}\nLine: '{line}'");
                continue;
            }

            if (useTileRange)
            {
                if (double.TryParse(parts[IDX_TMIN], NumberStyles.Float, ci, out double tmin) &&
                    double.TryParse(parts[IDX_TMAX], NumberStyles.Float, ci, out double tmax))
                {
                    tileMin = tmin;
                    tileMax = tmax;
                    tileRangeValid = true;
                }
                else
                {
                    Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid tile_min/tile_max; falling back to global range.");
                }
            }

            if (outRes != heightmapResolution)
                Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: out_res={outRes} but importer expects {heightmapResolution}.");

            double baseMin = tileRangeValid ? tileMin : gmin;
            double baseMax = tileRangeValid ? tileMax : gmax;
            float heightRange = (float)(baseMax - baseMin);
            if (heightRange <= 0.0001f)
            {
                if (tileRangeValid)
                {
                    Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: flat tile range; using epsilon height range.");
                    heightRange = 0.001f;
                }
                else
                {
                    skipped++;
                    Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: invalid height range (global_max <= global_min). Skipping.");
                    continue;
                }
            }

            string pngPath = Path.Combine(heightmapsDir, $"{tileId}.png").Replace("\\", "/");
            if (!File.Exists(pngPath))
            {
                skipped++;
                Debug.LogError($"[GeoTileImporter] Missing PNG for {tileId}: {pngPath}");
                continue;
            }

            EnsureHeightmapImportSettings(pngPath);

            var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(pngPath);
            if (tex == null)
            {
                skipped++;
                Debug.LogError($"[GeoTileImporter] Could not load Texture2D asset: {pngPath}");
                continue;
            }

            if (tex.width != heightmapResolution || tex.height != heightmapResolution)
                Debug.LogWarning($"[GeoTileImporter] Tile {tileId}: PNG {tex.width}x{tex.height}, expected {heightmapResolution}x{heightmapResolution}.");

            var terrainData = new TerrainData
            {
                heightmapResolution = heightmapResolution,
                size = new Vector3(tileSizeMeters, heightRange, tileSizeMeters),
            };

            int w = tex.width;
            int h = tex.height;
            var heights = new float[h, w];

            bool usedU16 = false;
            try
            {
                var raw = tex.GetPixelData<ushort>(0);
                if (raw.Length == w * h)
                {
                    for (int y = 0; y < h; y++)
                        for (int x = 0; x < w; x++)
                            heights[y, x] = raw[y * w + x] / 65535f;
                    usedU16 = true;
                }
            }
            catch
            {
                // fallback below
            }

            if (!usedU16)
            {
                var pixels = tex.GetPixels();
                for (int y = 0; y < h; y++)
                    for (int x = 0; x < w; x++)
                        heights[y, x] = pixels[y * w + x].r;
            }

            terrainData.SetHeights(0, 0, heights);

            var go = Terrain.CreateTerrainGameObject(terrainData);
            go.name = tileId;
            go.transform.parent = parent.transform;

            float ux = (float)(xmin - originX);
            float uz = (float)(ymin - originY);
            go.transform.position = new Vector3(ux, (float)baseMin, uz);

            var terrain = go.GetComponent<Terrain>();
            terrain.drawInstanced = true;

            // Optional ortho texture application
            if (applyOrthoTextures)
            {
                string orthoPath = Path.Combine(orthoDir, $"{tileId}.jpg").Replace("\\", "/");
                if (File.Exists(orthoPath))
                {
                    EnsureOrthoImportSettings(orthoPath);
                    var orthoTex = AssetDatabase.LoadAssetAtPath<Texture2D>(orthoPath);
                    if (orthoTex == null)
                    {
                        Debug.LogWarning($"[GeoTileImporter] Could not load ortho texture for {tileId}: {orthoPath}");
                    }
                    else
                    {
                        var layer = new TerrainLayer
                        {
                            diffuseTexture = orthoTex,
                            tileSize = new Vector2(tileSizeMeters, tileSizeMeters),
                            tileOffset = Vector2.zero
                        };
                        terrainData.terrainLayers = new[] { layer };
                        terrainData.alphamapResolution = 16;
                        var alpha = new float[terrainData.alphamapHeight, terrainData.alphamapWidth, 1];
                        for (int ay = 0; ay < terrainData.alphamapHeight; ay++)
                            for (int ax = 0; ax < terrainData.alphamapWidth; ax++)
                                alpha[ay, ax, 0] = 1f;
                        terrainData.SetAlphamaps(0, 0, alpha);
                        importedTextures++;
                    }
                }
                else
                {
                    Debug.LogWarning($"[GeoTileImporter] Ortho texture missing for {tileId}: {orthoPath}");
                }
            }

            Debug.Log($"[GeoTileImporter] Imported {tileId} @ XZ=({ux},{uz}) Y={baseMin} heightRange={heightRange} usedU16={usedU16}");
            imported++;
            placements.Add((tileId, ux, uz, (float)baseMin));
        }

        Debug.Log($"[GeoTileImporter] DONE. Imported={imported}, Skipped={skipped}, OrthoApplied={importedTextures} under '{parentName}'.");

        if (imported == 0)
            Debug.LogError("[GeoTileImporter] Imported 0 tiles. Scroll up for warnings/errors (missing columns, parse issues, missing PNGs).");

        ImportBuildings(placements);
        ImportTrees(placements);
        ImportFurniture(placements);
        ImportEnhancedTrees(placements);
    }

    private void ImportBuildings(List<(string tileId, float ux, float uz, float baseMin)> placements)
    {
        if (!importBuildings)
            return;

        // Choose directory based on enhanced toggle
        string activeDir = useEnhancedBuildings ? buildingsEnhancedDir : buildingsDir;
        string sourceLabel = useEnhancedBuildings ? "enhanced" : "standard";

        if (!Directory.Exists(activeDir))
        {
            Debug.LogWarning($"[GeoTileImporter] Buildings dir ({sourceLabel}) not found: {activeDir} (skipping buildings).");
            return;
        }

        var parent = GameObject.Find(buildingsParentName);
        if (parent == null) parent = new GameObject(buildingsParentName);
        if (deleteExistingBuildings)
        {
            for (int i = parent.transform.childCount - 1; i >= 0; i--)
                DestroyImmediate(parent.transform.GetChild(i).gameObject);
        }

        int imported = 0, missing = 0;
        foreach (var (tileId, ux, uz, baseMin) in placements)
        {
            string glbPath = Path.Combine(activeDir, $"{tileId}.glb").Replace("\\", "/");
            if (!File.Exists(glbPath))
            {
                missing++;
                Debug.LogWarning($"[GeoTileImporter] Building GLB ({sourceLabel}) missing for {tileId}: {glbPath}");
                continue;
            }

            var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(glbPath);
            if (prefab == null)
            {
                missing++;
                Debug.LogWarning($"[GeoTileImporter] Could not load building GLB for {tileId}: {glbPath}");
                continue;
            }

            var inst = PrefabUtility.InstantiatePrefab(prefab) as GameObject ?? Instantiate(prefab);
            inst.name = tileId;
            inst.transform.SetParent(parent.transform, false);
            inst.transform.position = new Vector3(ux, baseMin, uz);
            inst.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
            inst.isStatic = true;
            imported++;
        }

        Debug.Log($"[GeoTileImporter] Buildings ({sourceLabel}) imported={imported}, missing/failed={missing} under '{buildingsParentName}'.");
    }

    private void ImportTrees(List<(string tileId, float ux, float uz, float baseMin)> placements)
    {
        if (!importTrees)
            return;

        if (!Directory.Exists(treesDir))
        {
            Debug.LogWarning($"[GeoTileImporter] Trees dir not found: {treesDir} (skipping trees).");
            return;
        }

        var parent = GameObject.Find(treesParentName);
        if (parent == null) parent = new GameObject(treesParentName);
        if (deleteExistingTrees)
        {
            for (int i = parent.transform.childCount - 1; i >= 0; i--)
                DestroyImmediate(parent.transform.GetChild(i).gameObject);
        }

        if (!File.Exists(treeProxyPath))
        {
            Debug.LogWarning($"[GeoTileImporter] Tree proxy GLB not found (for reference/materials): {treeProxyPath}");
        }

        int importedTiles = 0, importedChunks = 0, missingTiles = 0;
        foreach (var (tileId, ux, uz, baseMin) in placements)
        {
            // Look for chunk files: {tileId}_0_0.glb, {tileId}_0_1.glb, etc.
            // Standard tree export creates 4x4 chunks per tile
            var chunkFiles = Directory.GetFiles(treesDir, $"{tileId}_*.glb");

            if (chunkFiles.Length == 0)
            {
                // Try single file as fallback
                string singlePath = Path.Combine(treesDir, $"{tileId}.glb").Replace("\\", "/");
                if (File.Exists(singlePath))
                {
                    chunkFiles = new[] { singlePath };
                }
                else
                {
                    missingTiles++;
                    continue;
                }
            }

            // Create container for this tile's tree chunks
            var tileContainer = new GameObject($"Trees_{tileId}");
            tileContainer.transform.SetParent(parent.transform, false);
            tileContainer.transform.position = new Vector3(ux, baseMin, uz);
            tileContainer.transform.localRotation = Quaternion.Euler(0f, 180f, 0f);
            tileContainer.isStatic = true;

            foreach (var chunkPath in chunkFiles)
            {
                string assetPath = chunkPath.Replace("\\", "/");
                var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
                if (prefab == null)
                {
                    Debug.LogWarning($"[GeoTileImporter] Could not load tree chunk: {assetPath}");
                    continue;
                }

                var inst = PrefabUtility.InstantiatePrefab(prefab) as GameObject ?? Instantiate(prefab);
                inst.name = Path.GetFileNameWithoutExtension(chunkPath);
                inst.transform.SetParent(tileContainer.transform, false);
                inst.transform.localPosition = Vector3.zero; // Chunks already have correct local positions
                inst.isStatic = true;
                importedChunks++;
            }

            importedTiles++;
        }

        Debug.Log($"[GeoTileImporter] Trees imported: {importedTiles} tiles, {importedChunks} chunks, {missingTiles} missing under '{treesParentName}'.");
    }

    private void ImportFurniture(List<(string tileId, float ux, float uz, float baseMin)> placements)
    {
        if (!importFurniture)
            return;

        if (!Directory.Exists(furnitureDir))
        {
            Debug.LogWarning($"[GeoTileImporter] Furniture dir not found: {furnitureDir} (skipping furniture).");
            return;
        }

        var parent = GameObject.Find(furnitureParentName);
        if (parent == null) parent = new GameObject(furnitureParentName);
        if (deleteExistingFurniture)
        {
            for (int i = parent.transform.childCount - 1; i >= 0; i--)
                DestroyImmediate(parent.transform.GetChild(i).gameObject);
        }

        int imported = 0, skipped = 0;
        var ci = CultureInfo.InvariantCulture;

        foreach (var (tileId, ux, uz, baseMin) in placements)
        {
            string csvPath = Path.Combine(furnitureDir, $"{tileId}.csv").Replace("\\", "/");
            if (!File.Exists(csvPath))
            {
                continue; // No furniture for this tile
            }

            var lines = File.ReadAllLines(csvPath);
            if (lines.Length < 2)
                continue;

            // Parse header
            var header = lines[0].Split(',');
            int idxXLocal = -1, idxYLocal = -1, idxZGround = -1, idxHeight = -1, idxType = -1;
            for (int i = 0; i < header.Length; i++)
            {
                var col = header[i].Trim().ToLowerInvariant();
                if (col == "x_local") idxXLocal = i;
                else if (col == "y_local") idxYLocal = i;
                else if (col == "z_ground") idxZGround = i;
                else if (col == "height") idxHeight = i;
                else if (col == "type") idxType = i;
            }

            if (idxXLocal < 0 || idxYLocal < 0 || idxType < 0)
            {
                Debug.LogWarning($"[GeoTileImporter] Furniture CSV missing required columns: {csvPath}");
                continue;
            }

            // Create tile container
            var tileContainer = new GameObject($"Furniture_{tileId}");
            tileContainer.transform.SetParent(parent.transform, false);
            tileContainer.transform.position = new Vector3(ux, baseMin, uz);
            tileContainer.isStatic = true;

            for (int i = 1; i < lines.Length; i++)
            {
                var parts = lines[i].Split(',');
                if (parts.Length <= Math.Max(Math.Max(idxXLocal, idxYLocal), idxType))
                {
                    skipped++;
                    continue;
                }

                try
                {
                    float xLocal = float.Parse(parts[idxXLocal].Trim(), ci);
                    float yLocal = float.Parse(parts[idxYLocal].Trim(), ci);
                    float zGround = idxZGround >= 0 && idxZGround < parts.Length
                        ? float.Parse(parts[idxZGround].Trim(), ci)
                        : 0f;
                    float height = idxHeight >= 0 && idxHeight < parts.Length
                        ? float.Parse(parts[idxHeight].Trim(), ci)
                        : 1f;
                    string furnitureType = parts[idxType].Trim().ToLowerInvariant();

                    // Select prefab based on type, or create placeholder
                    GameObject obj = null;
                    GameObject prefabToUse = null;
                    Vector3 fallbackScale = Vector3.one;

                    switch (furnitureType)
                    {
                        case "lamp":
                            prefabToUse = lampPrefab;
                            fallbackScale = new Vector3(0.3f, height, 0.3f);
                            break;
                        case "bench":
                            prefabToUse = benchPrefab;
                            fallbackScale = new Vector3(1.5f, height, 0.5f);
                            break;
                        case "sign":
                            prefabToUse = signPrefab;
                            fallbackScale = new Vector3(0.1f, height, 0.5f);
                            break;
                        case "bollard":
                            prefabToUse = bollardPrefab;
                            fallbackScale = new Vector3(0.2f, height, 0.2f);
                            break;
                        default:
                            prefabToUse = defaultFurniturePrefab;
                            fallbackScale = new Vector3(0.5f, height, 0.5f);
                            break;
                    }

                    if (prefabToUse != null)
                    {
                        // Use prefab
                        obj = PrefabUtility.InstantiatePrefab(prefabToUse) as GameObject ?? Instantiate(prefabToUse);
                        obj.name = $"{furnitureType}_{i}";
                        // Scale prefab to match height (assuming prefab is normalized to ~1m)
                        float scaleMultiplier = height;
                        obj.transform.localScale = Vector3.one * scaleMultiplier;
                    }
                    else
                    {
                        // Create placeholder cube
                        obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
                        obj.name = $"{furnitureType}_{i}";
                        obj.transform.localScale = fallbackScale;
                    }

                    obj.transform.SetParent(tileContainer.transform, false);
                    obj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
                    obj.isStatic = true;
                    imported++;
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"[GeoTileImporter] Failed to parse furniture line {i} in {csvPath}: {e.Message}");
                    skipped++;
                }
            }
        }

        Debug.Log($"[GeoTileImporter] Furniture imported={imported}, skipped={skipped} under '{furnitureParentName}'.");
    }

    private void ImportEnhancedTrees(List<(string tileId, float ux, float uz, float baseMin)> placements)
    {
        if (!importEnhancedTrees)
            return;

        if (!Directory.Exists(enhancedTreesDir))
        {
            Debug.LogWarning($"[GeoTileImporter] Enhanced trees dir not found: {enhancedTreesDir} (skipping enhanced trees).");
            return;
        }

        var parent = GameObject.Find(enhancedTreesParentName);
        if (parent == null) parent = new GameObject(enhancedTreesParentName);
        if (deleteExistingEnhancedTrees)
        {
            for (int i = parent.transform.childCount - 1; i >= 0; i--)
                DestroyImmediate(parent.transform.GetChild(i).gameObject);
        }

        int imported = 0, skipped = 0;
        var ci = CultureInfo.InvariantCulture;

        foreach (var (tileId, ux, uz, baseMin) in placements)
        {
            string csvPath = Path.Combine(enhancedTreesDir, $"{tileId}.csv").Replace("\\", "/");
            if (!File.Exists(csvPath))
            {
                continue; // No enhanced trees for this tile
            }

            var lines = File.ReadAllLines(csvPath);
            if (lines.Length < 2)
                continue;

            // Parse header
            var header = lines[0].Split(',');
            int idxXLocal = -1, idxYLocal = -1, idxZGround = -1, idxHeight = -1, idxRadius = -1;
            int idxCanopyR = -1, idxCanopyG = -1, idxCanopyB = -1;
            for (int i = 0; i < header.Length; i++)
            {
                var col = header[i].Trim().ToLowerInvariant();
                if (col == "x_local") idxXLocal = i;
                else if (col == "y_local") idxYLocal = i;
                else if (col == "z_ground") idxZGround = i;
                else if (col == "height") idxHeight = i;
                else if (col == "radius") idxRadius = i;
                else if (col == "canopy_r") idxCanopyR = i;
                else if (col == "canopy_g") idxCanopyG = i;
                else if (col == "canopy_b") idxCanopyB = i;
            }

            if (idxXLocal < 0 || idxYLocal < 0 || idxHeight < 0)
            {
                Debug.LogWarning($"[GeoTileImporter] Enhanced trees CSV missing required columns: {csvPath}");
                continue;
            }

            // Create tile container
            var tileContainer = new GameObject($"Trees_{tileId}");
            tileContainer.transform.SetParent(parent.transform, false);
            tileContainer.transform.position = new Vector3(ux, baseMin, uz);
            tileContainer.isStatic = true;

            for (int i = 1; i < lines.Length; i++)
            {
                var parts = lines[i].Split(',');
                if (parts.Length <= Math.Max(Math.Max(idxXLocal, idxYLocal), idxHeight))
                {
                    skipped++;
                    continue;
                }

                try
                {
                    float xLocal = float.Parse(parts[idxXLocal].Trim(), ci);
                    float yLocal = float.Parse(parts[idxYLocal].Trim(), ci);
                    float zGround = idxZGround >= 0 && idxZGround < parts.Length
                        ? float.Parse(parts[idxZGround].Trim(), ci)
                        : 0f;
                    float height = float.Parse(parts[idxHeight].Trim(), ci);
                    float radius = idxRadius >= 0 && idxRadius < parts.Length
                        ? float.Parse(parts[idxRadius].Trim(), ci)
                        : height * 0.25f;

                    // Parse canopy color (default to green if missing)
                    int canopyR = 128, canopyG = 160, canopyB = 80;
                    if (idxCanopyR >= 0 && idxCanopyR < parts.Length)
                        canopyR = Mathf.Clamp(int.Parse(parts[idxCanopyR].Trim()), 0, 255);
                    if (idxCanopyG >= 0 && idxCanopyG < parts.Length)
                        canopyG = Mathf.Clamp(int.Parse(parts[idxCanopyG].Trim()), 0, 255);
                    if (idxCanopyB >= 0 && idxCanopyB < parts.Length)
                        canopyB = Mathf.Clamp(int.Parse(parts[idxCanopyB].Trim()), 0, 255);

                    Color canopyColor = new Color(canopyR / 255f, canopyG / 255f, canopyB / 255f);

                    GameObject treeObj;
                    if (treePrefab != null)
                    {
                        // Use prefab instance
                        treeObj = PrefabUtility.InstantiatePrefab(treePrefab) as GameObject ?? Instantiate(treePrefab);
                        treeObj.name = $"tree_{i}";

                        // Scale prefab based on height/radius
                        float scaleY = height / 10f; // Assuming prefab is ~10m tall
                        float scaleXZ = radius * 2f / 5f; // Assuming prefab canopy is ~5m wide
                        treeObj.transform.localScale = new Vector3(scaleXZ, scaleY, scaleXZ);

                        // Apply canopy color to materials
                        var renderers = treeObj.GetComponentsInChildren<Renderer>();
                        foreach (var rend in renderers)
                        {
                            foreach (var mat in rend.sharedMaterials)
                            {
                                if (mat != null)
                                {
                                    // Create material instance to avoid modifying shared material
                                    var matInst = new Material(mat);
                                    matInst.color = canopyColor;
                                    rend.material = matInst;
                                }
                            }
                        }
                    }
                    else
                    {
                        // Create procedural tree placeholder (cylinder trunk + sphere canopy)
                        treeObj = new GameObject($"tree_{i}");

                        // Trunk (cylinder)
                        var trunk = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
                        trunk.name = "trunk";
                        trunk.transform.SetParent(treeObj.transform, false);
                        trunk.transform.localPosition = new Vector3(0, height * 0.25f, 0);
                        trunk.transform.localScale = new Vector3(0.3f, height * 0.5f, 0.3f);
                        var trunkRend = trunk.GetComponent<Renderer>();
                        if (trunkRend != null)
                        {
                            var trunkMat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
                            trunkMat.color = new Color(0.4f, 0.25f, 0.15f); // Brown
                            trunkRend.material = trunkMat;
                        }

                        // Canopy (sphere)
                        var canopy = GameObject.CreatePrimitive(PrimitiveType.Sphere);
                        canopy.name = "canopy";
                        canopy.transform.SetParent(treeObj.transform, false);
                        canopy.transform.localPosition = new Vector3(0, height * 0.7f, 0);
                        canopy.transform.localScale = new Vector3(radius * 2f, height * 0.5f, radius * 2f);
                        var canopyRend = canopy.GetComponent<Renderer>();
                        if (canopyRend != null)
                        {
                            var canopyMat = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
                            canopyMat.color = canopyColor;
                            canopyRend.material = canopyMat;
                        }
                    }

                    treeObj.transform.SetParent(tileContainer.transform, false);
                    treeObj.transform.localPosition = new Vector3(xLocal, zGround - baseMin, yLocal);
                    treeObj.isStatic = true;
                    imported++;
                }
                catch (Exception e)
                {
                    Debug.LogWarning($"[GeoTileImporter] Failed to parse enhanced tree line {i} in {csvPath}: {e.Message}");
                    skipped++;
                }
            }
        }

        Debug.Log($"[GeoTileImporter] Enhanced trees imported={imported}, skipped={skipped} under '{enhancedTreesParentName}'.");
    }
}
