import math
import random
import struct
from pathlib import Path

import unreal


MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
PROJECT_ROOT = Path(r"Z:\AgrarianGameBulid")
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
HEIGHTMAP_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_1009.r16"
LANDSCAPE_SIZE = 1009
XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1)
Z_SCALE_CM = 100.0
LANDSCAPE_MIN_XY = -50000.0

FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
FOLIAGE_RANDOM_SEED = 4160544
PLACEHOLDER_MESH_FOLDER = "/Game/Agrarian/Environment/PlaceholderMeshes"
PLACEHOLDER_MESH_SOURCES = {
    "SM_AGR_Placeholder_Cube": "/Game/LevelPrototyping/Meshes/SM_Cube",
    "SM_AGR_Placeholder_ChamferCube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube",
    "SM_AGR_Placeholder_Cylinder": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
    "SM_AGR_Placeholder_QuarterCylinder": "/Game/LevelPrototyping/Meshes/SM_QuarterCylinder",
    "SM_AGR_Placeholder_Plane": "/Game/LevelPrototyping/Meshes/SM_Plane",
}
PLACEHOLDER_MESHES = {
    name: f"{PLACEHOLDER_MESH_FOLDER}/{name}"
    for name in PLACEHOLDER_MESH_SOURCES
}
FOLIAGE_MESHES = {
    "tree": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
    "shrub": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
    "grass": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
}
VARIATION_MESHES = {
    "cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cube"],
    "chamfer_cube": PLACEHOLDER_MESHES["SM_AGR_Placeholder_ChamferCube"],
    "cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Cylinder"],
    "quarter_cylinder": PLACEHOLDER_MESHES["SM_AGR_Placeholder_QuarterCylinder"],
    "plane": PLACEHOLDER_MESHES["SM_AGR_Placeholder_Plane"],
}
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = {
    "terrain": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
        "color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0),
        "roughness": 0.92,
    },
    "tree": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
        "color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0),
        "roughness": 0.88,
    },
    "shrub": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
        "color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0),
        "roughness": 0.9,
    },
    "grass": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
        "color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0),
        "roughness": 0.95,
    },
    "wood_resource": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
        "color": unreal.LinearColor(0.26, 0.16, 0.08, 1.0),
        "roughness": 0.86,
    },
    "fiber_resource": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Fiber_Resource",
        "color": unreal.LinearColor(0.55, 0.49, 0.28, 1.0),
        "roughness": 0.93,
    },
    "edible_plant_resource": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_EdiblePlant_Resource",
        "color": unreal.LinearColor(0.22, 0.46, 0.18, 1.0),
        "roughness": 0.9,
    },
    "stone_resource": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Stone_Sandstone",
        "color": unreal.LinearColor(0.43, 0.40, 0.35, 1.0),
        "roughness": 0.97,
    },
    "fresh_water": {
        "path": f"{MATERIAL_FOLDER}/M_AGR_GZ_FreshWater",
        "color": unreal.LinearColor(0.08, 0.28, 0.38, 1.0),
        "roughness": 0.35,
    },
}
RESOURCE_MATERIAL_BY_LABEL_PREFIX = {
    "AGR_GZ_Wood": "wood_resource",
    "AGR_GZ_Fiber": "fiber_resource",
    "AGR_GZ_EdiblePlant": "edible_plant_resource",
    "AGR_GZ_Stone": "stone_resource",
    "AGR_DemoWoodResource": "wood_resource",
    "AGR_DemoFiberResource": "fiber_resource",
    "AGR_GZ_FreshWaterSource": "fresh_water",
}

SAFE_SPAWN_CONFIG = {
    "player_start_label": "AGR_DemoPlayerStart",
    "safe_z_offset": 220.0,
    "min_elevation_m": 2.0,
    "max_slope_degrees": 8.0,
    "slope_sample_distance_cm": 500.0,
    "min_water_distance_cm": 10000.0,
    "min_resource_distance_cm": 5000.0,
    "fallback_location_xy": unreal.Vector(-22000.0, -3500.0, 0.0),
    "candidate_locations_xy": [
        unreal.Vector(-22000.0, -3500.0, 0.0),
        unreal.Vector(-25000.0, -4000.0, 0.0),
        unreal.Vector(-20500.0, -10500.0, 0.0),
        unreal.Vector(-28000.0, -9000.0, 0.0),
        unreal.Vector(-30000.0, -12000.0, 0.0),
        unreal.Vector(-35000.0, 0.0, 0.0),
    ],
}

MAP_BOUNDARY_CONFIG = {
    "label": "AGR_GroundZeroMapBoundary",
    "boundary_id": "ground_zero_mvp_tile",
    "location": unreal.Vector(0.0, 0.0, 10000.0),
    "extent": unreal.Vector(50000.0, 50000.0, 25000.0),
    "padding_cm": 250.0,
    "warning_distance_cm": 3000.0,
}


DEMO_ACTORS = [
    {
        "label": "AGR_DemoPlayerStart",
        "class": unreal.PlayerStart,
        "location_xy": unreal.Vector(-18500.0, -6400.0, 0.0),
        "z_offset": 220.0,
        "rotation": unreal.Rotator(0.0, 72.0, 0.0),
    },
    {
        "label": "AGR_DemoWoodResource_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
        "location_xy": unreal.Vector(-16800.0, -5400.0, 0.0),
        "z_offset": 80.0,
        "rotation": unreal.Rotator(0.0, 25.0, 0.0),
    },
    {
        "label": "AGR_DemoFiberResource_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
        "location_xy": unreal.Vector(-15600.0, -7850.0, 0.0),
        "z_offset": 80.0,
        "rotation": unreal.Rotator(0.0, -15.0, 0.0),
    },
    {
        "label": "AGR_DemoCampfire_01",
        "class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
        "location_xy": unreal.Vector(-13200.0, -6350.0, 0.0),
        "z_offset": 90.0,
        "rotation": unreal.Rotator(0.0, 0.0, 0.0),
    },
    {
        "label": "AGR_DemoPrimitiveShelter_01",
        "class_path": "/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter",
        "location_xy": unreal.Vector(-11500.0, -7750.0, 0.0),
        "z_offset": 120.0,
        "rotation": unreal.Rotator(0.0, -30.0, 0.0),
    },
    {
        "label": "AGR_DemoRabbitWildlife_01",
        "class_path": "/Game/Agrarian/Blueprints/Wildlife/BP_RabbitWildlife",
        "location_xy": unreal.Vector(-19800.0, -8550.0, 0.0),
        "z_offset": 120.0,
        "rotation": unreal.Rotator(0.0, 135.0, 0.0),
    },
    {
        "label": "AGR_DemoSkyLightingController",
        "class": unreal.AgrarianSkyLightingController,
        "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
        "fixed_z": 12000.0,
        "rotation": unreal.Rotator(-42.0, -35.0, 0.0),
    },
    {
        "label": "AGR_DemoWeatherAudioController",
        "class": unreal.AgrarianWeatherAudioController,
        "location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
        "fixed_z": 11800.0,
        "rotation": unreal.Rotator(0.0, 0.0, 0.0),
    },
    {
        "label": "AGR_DemoNoticeActor",
        "class": unreal.AgrarianDemoNoticeActor,
        "location_xy": unreal.Vector(-18500.0, -6400.0, 0.0),
        "fixed_z": 1600.0,
        "rotation": unreal.Rotator(0.0, 0.0, 0.0),
    },
]

LEGACY_DEMO_LIGHTING_LABELS = {
    "AGR_DemoSun",
    "AGR_DemoSkyLight",
    "AGR_DemoFog",
}


BIOME_RESOURCE_ACTORS = [
    {
        "label": "AGR_GZ_Wood_CoastalScrub_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
        "location_xy": unreal.Vector(-28600.0, 7400.0, 0.0),
        "z_offset": 70.0,
        "rotation": unreal.Rotator(0.0, 18.0, 0.0),
    },
    {
        "label": "AGR_GZ_Wood_CoastalScrub_02",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
        "location_xy": unreal.Vector(-5400.0, 21400.0, 0.0),
        "z_offset": 70.0,
        "rotation": unreal.Rotator(0.0, -42.0, 0.0),
    },
    {
        "label": "AGR_GZ_Wood_Hillside_03",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
        "location_xy": unreal.Vector(18400.0, 32200.0, 0.0),
        "z_offset": 70.0,
        "rotation": unreal.Rotator(0.0, 86.0, 0.0),
    },
    {
        "label": "AGR_GZ_Fiber_Grassland_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
        "location_xy": unreal.Vector(-33400.0, -16200.0, 0.0),
        "z_offset": 65.0,
        "rotation": unreal.Rotator(0.0, 6.0, 0.0),
    },
    {
        "label": "AGR_GZ_Fiber_Grassland_02",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
        "location_xy": unreal.Vector(8200.0, -18200.0, 0.0),
        "z_offset": 65.0,
        "rotation": unreal.Rotator(0.0, -28.0, 0.0),
    },
    {
        "label": "AGR_GZ_Fiber_Scrub_03",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
        "location_xy": unreal.Vector(29200.0, -4200.0, 0.0),
        "z_offset": 65.0,
        "rotation": unreal.Rotator(0.0, 54.0, 0.0),
    },
    {
        "label": "AGR_GZ_Fiber_DrainageCandidate_04",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
        "location_xy": unreal.Vector(-6200.0, 9200.0, 0.0),
        "z_offset": 65.0,
        "rotation": unreal.Rotator(0.0, 114.0, 0.0),
    },
    {
        "label": "AGR_GZ_EdiblePlant_CoastalScrub_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
        "location_xy": unreal.Vector(-31400.0, -7600.0, 0.0),
        "z_offset": 58.0,
        "rotation": unreal.Rotator(0.0, 21.0, 0.0),
    },
    {
        "label": "AGR_GZ_EdiblePlant_Grassland_02",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
        "location_xy": unreal.Vector(6200.0, -26800.0, 0.0),
        "z_offset": 58.0,
        "rotation": unreal.Rotator(0.0, -48.0, 0.0),
    },
    {
        "label": "AGR_GZ_EdiblePlant_DrainageCandidate_03",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_EdiblePlantResourceNode",
        "location_xy": unreal.Vector(-9400.0, 12800.0, 0.0),
        "z_offset": 58.0,
        "rotation": unreal.Rotator(0.0, 96.0, 0.0),
    },
    {
        "label": "AGR_GZ_Stone_Slope_01",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
        "location_xy": unreal.Vector(22600.0, 17600.0, 0.0),
        "z_offset": 45.0,
        "rotation": unreal.Rotator(0.0, 12.0, 0.0),
    },
    {
        "label": "AGR_GZ_Stone_Slope_02",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
        "location_xy": unreal.Vector(37600.0, 28600.0, 0.0),
        "z_offset": 45.0,
        "rotation": unreal.Rotator(0.0, -36.0, 0.0),
    },
    {
        "label": "AGR_GZ_Stone_ExposedTerrain_03",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
        "location_xy": unreal.Vector(11800.0, 38200.0, 0.0),
        "z_offset": 45.0,
        "rotation": unreal.Rotator(0.0, 71.0, 0.0),
    },
    {
        "label": "AGR_GZ_Stone_ValleyEdge_04",
        "class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
        "location_xy": unreal.Vector(-24800.0, 28600.0, 0.0),
        "z_offset": 45.0,
        "rotation": unreal.Rotator(0.0, 133.0, 0.0),
    },
]


WATER_SOURCE_ACTORS = [
    {
        "label": "AGR_GZ_FreshWaterSource_01",
        "class_path": "/Game/Agrarian/Blueprints/World/BP_FreshWaterSource",
        "location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
        "z_offset": 36.0,
        "rotation": unreal.Rotator(0.0, 0.0, 0.0),
    },
]

WEATHER_EXPOSURE_ZONES = [
    {
        "label": "AGR_GZ_WeatherExposure_Ridge_01",
        "zone_id": "ground_zero_ridge_exposed",
        "location_xy": unreal.Vector(36000.0, 29200.0, 0.0),
        "z_offset": 450.0,
        "extent": unreal.Vector(11500.0, 9200.0, 2200.0),
        "exposure_multiplier": 1.45,
        "temperature_offset_c": -1.8,
    },
    {
        "label": "AGR_GZ_WeatherExposure_CoastalWind_01",
        "zone_id": "ground_zero_coastal_wind",
        "location_xy": unreal.Vector(-31500.0, -14600.0, 0.0),
        "z_offset": 350.0,
        "extent": unreal.Vector(10500.0, 7600.0, 1600.0),
        "exposure_multiplier": 1.25,
        "temperature_offset_c": -0.9,
    },
    {
        "label": "AGR_GZ_WeatherExposure_DrainageCool_01",
        "zone_id": "ground_zero_drainage_cool",
        "location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
        "z_offset": 260.0,
        "extent": unreal.Vector(7200.0, 5400.0, 1100.0),
        "exposure_multiplier": 1.1,
        "temperature_offset_c": -1.2,
    },
]

ENVIRONMENT_VARIATION_ACTORS = [
    {
        "label": "AGR_GZ_EnvVar_Tree_Canopy_01",
        "mesh_key": "chamfer_cube",
        "material_key": "tree",
        "location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
        "z_offset": 390.0,
        "scale": unreal.Vector(2.5, 2.0, 1.6),
        "rotation": unreal.Rotator(0.0, 32.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Tree_Trunk_01",
        "mesh_key": "cylinder",
        "material_key": "wood_resource",
        "location_xy": unreal.Vector(-27500.0, 6900.0, 0.0),
        "z_offset": 170.0,
        "scale": unreal.Vector(0.35, 0.35, 3.6),
        "rotation": unreal.Rotator(0.0, 32.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Tree_Canopy_02",
        "mesh_key": "chamfer_cube",
        "material_key": "tree",
        "location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
        "z_offset": 430.0,
        "scale": unreal.Vector(2.0, 2.8, 1.45),
        "rotation": unreal.Rotator(0.0, -18.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Tree_Trunk_02",
        "mesh_key": "cylinder",
        "material_key": "wood_resource",
        "location_xy": unreal.Vector(17600.0, 31800.0, 0.0),
        "z_offset": 190.0,
        "scale": unreal.Vector(0.3, 0.3, 4.1),
        "rotation": unreal.Rotator(0.0, -18.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Bush_Rounded_01",
        "mesh_key": "chamfer_cube",
        "material_key": "shrub",
        "location_xy": unreal.Vector(-33400.0, -15200.0, 0.0),
        "z_offset": 70.0,
        "scale": unreal.Vector(1.9, 1.4, 0.7),
        "rotation": unreal.Rotator(0.0, 11.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Bush_Rounded_02",
        "mesh_key": "chamfer_cube",
        "material_key": "shrub",
        "location_xy": unreal.Vector(30400.0, -3900.0, 0.0),
        "z_offset": 75.0,
        "scale": unreal.Vector(1.45, 2.1, 0.85),
        "rotation": unreal.Rotator(0.0, 73.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Grass_Mat_01",
        "mesh_key": "plane",
        "material_key": "grass",
        "location_xy": unreal.Vector(8200.0, -17100.0, 0.0),
        "z_offset": 14.0,
        "scale": unreal.Vector(3.6, 2.2, 1.0),
        "rotation": unreal.Rotator(0.0, -34.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Grass_Mat_02",
        "mesh_key": "plane",
        "material_key": "grass",
        "location_xy": unreal.Vector(-6200.0, 8300.0, 0.0),
        "z_offset": 14.0,
        "scale": unreal.Vector(2.7, 4.0, 1.0),
        "rotation": unreal.Rotator(0.0, 41.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Rock_Slab_01",
        "mesh_key": "chamfer_cube",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(22900.0, 18300.0, 0.0),
        "z_offset": 45.0,
        "scale": unreal.Vector(1.8, 1.05, 0.45),
        "rotation": unreal.Rotator(0.0, 24.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Rock_Slab_02",
        "mesh_key": "quarter_cylinder",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(38100.0, 29300.0, 0.0),
        "z_offset": 55.0,
        "scale": unreal.Vector(1.2, 1.8, 0.75),
        "rotation": unreal.Rotator(0.0, -47.0, 0.0),
    },
    {
        "label": "AGR_GZ_EnvVar_Water_Surface_01",
        "mesh_key": "plane",
        "material_key": "fresh_water",
        "location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
        "z_offset": 18.0,
        "scale": unreal.Vector(2.6, 1.6, 1.0),
        "rotation": unreal.Rotator(0.0, -15.0, 0.0),
    },
]

RUIN_PLACEHOLDER_ACTORS = [
    {
        "label": "AGR_GZ_Landmark_Ruin_Foundation_01",
        "mesh_key": "chamfer_cube",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(6400.0, 24700.0, 0.0),
        "z_offset": 38.0,
        "scale": unreal.Vector(4.8, 3.2, 0.22),
        "rotation": unreal.Rotator(0.0, -22.0, 0.0),
    },
    {
        "label": "AGR_GZ_Landmark_Ruin_Wall_01",
        "mesh_key": "chamfer_cube",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(5750.0, 25350.0, 0.0),
        "z_offset": 165.0,
        "scale": unreal.Vector(0.42, 2.35, 1.65),
        "rotation": unreal.Rotator(0.0, -22.0, 0.0),
    },
    {
        "label": "AGR_GZ_Landmark_Ruin_Wall_02",
        "mesh_key": "chamfer_cube",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(7250.0, 24250.0, 0.0),
        "z_offset": 118.0,
        "scale": unreal.Vector(0.36, 1.7, 1.05),
        "rotation": unreal.Rotator(0.0, 68.0, 0.0),
    },
    {
        "label": "AGR_GZ_Landmark_Ruin_Cairn_01",
        "mesh_key": "cylinder",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(7900.0, 25300.0, 0.0),
        "z_offset": 92.0,
        "scale": unreal.Vector(0.9, 0.9, 1.15),
        "rotation": unreal.Rotator(0.0, 11.0, 0.0),
    },
    {
        "label": "AGR_GZ_Landmark_Ruin_Threshold_01",
        "mesh_key": "quarter_cylinder",
        "material_key": "stone_resource",
        "location_xy": unreal.Vector(6550.0, 23550.0, 0.0),
        "z_offset": 70.0,
        "scale": unreal.Vector(1.8, 0.55, 0.5),
        "rotation": unreal.Rotator(0.0, -22.0, 0.0),
    },
]


FOLIAGE_ZONES = {
    "trees": {
        "count": 42,
        "x_range": (-25000.0, 42000.0),
        "y_range": (-12000.0, 42000.0),
        "min_elevation_m": 18.0,
        "avoid_radius_cm": 3600.0,
        "scale_range": (0.75, 1.35),
    },
    "shrubs": {
        "count": 96,
        "x_range": (-42000.0, 45000.0),
        "y_range": (-38000.0, 45000.0),
        "min_elevation_m": 7.0,
        "avoid_radius_cm": 2600.0,
        "scale_range": (0.45, 1.05),
    },
    "grass": {
        "count": 180,
        "x_range": (-46000.0, 46000.0),
        "y_range": (-46000.0, 46000.0),
        "min_elevation_m": 4.0,
        "avoid_radius_cm": 1800.0,
        "scale_range": (0.35, 0.9),
    },
}


def get_actor_label(actor):
    try:
        return actor.get_actor_label()
    except Exception:
        return actor.get_name()


def set_actor_label(actor, label):
    try:
        actor.set_actor_label(label, mark_dirty=True)
    except TypeError:
        actor.set_actor_label(label)


def load_blueprint_class(path):
    generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path)
    if not generated_class:
        raise RuntimeError(f"Could not load Blueprint class: {path}")
    return generated_class


def load_required_asset(path):
    asset = unreal.EditorAssetLibrary.load_asset(path)
    if not asset:
        raise RuntimeError(f"Required asset not found: {path}")
    return asset


def ensure_native_placeholder_meshes():
    unreal.EditorAssetLibrary.make_directory(PLACEHOLDER_MESH_FOLDER)
    for asset_name, source_path in PLACEHOLDER_MESH_SOURCES.items():
        destination_path = f"{PLACEHOLDER_MESH_FOLDER}/{asset_name}"
        if unreal.EditorAssetLibrary.does_asset_exist(destination_path):
            continue

        if not unreal.EditorAssetLibrary.duplicate_asset(source_path, destination_path):
            raise RuntimeError(f"Could not create native placeholder mesh {destination_path} from {source_path}")
        unreal.EditorAssetLibrary.save_asset(destination_path)
        unreal.log(f"Created native placeholder mesh: {destination_path}")


def ensure_environment_materials():
    if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
        unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)

    asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
    created_or_loaded = {}
    for key, spec in ENVIRONMENT_MATERIALS.items():
        material = unreal.EditorAssetLibrary.load_asset(spec["path"])
        if not material:
            asset_name = spec["path"].rsplit("/", 1)[-1]
            material = asset_tools.create_asset(asset_name, MATERIAL_FOLDER, unreal.Material, unreal.MaterialFactoryNew())
            if not material:
                raise RuntimeError(f"Could not create material: {spec['path']}")

            base_color = unreal.MaterialEditingLibrary.create_material_expression(
                material, unreal.MaterialExpressionConstant3Vector, -420, -120
            )
            base_color.set_editor_property("constant", spec["color"])
            unreal.MaterialEditingLibrary.connect_material_property(
                base_color, "", unreal.MaterialProperty.MP_BASE_COLOR
            )

            roughness = unreal.MaterialEditingLibrary.create_material_expression(
                material, unreal.MaterialExpressionConstant, -420, 80
            )
            roughness.set_editor_property("r", spec["roughness"])
            unreal.MaterialEditingLibrary.connect_material_property(
                roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
            )

            unreal.MaterialEditingLibrary.recompile_material(material)
            unreal.EditorAssetLibrary.save_asset(spec["path"])
            unreal.log(f"Created Ground Zero environment material: {spec['path']}")
        created_or_loaded[key] = material

    return created_or_loaded


def apply_material_to_actor_meshes(actor, material):
    applied_count = 0
    for component in actor.get_components_by_class(unreal.StaticMeshComponent):
        slot_count = component.get_num_materials()
        if slot_count == 0:
            component.set_material(0, material)
            applied_count += 1
            continue
        for slot_index in range(slot_count):
            component.set_material(slot_index, material)
            applied_count += 1
    return applied_count


def material_key_for_actor_label(label):
    for prefix, material_key in RESOURCE_MATERIAL_BY_LABEL_PREFIX.items():
        if label.startswith(prefix):
            return material_key
    return None


def apply_landscape_material(material):
    applied = 0
    for actor in unreal.EditorLevelLibrary.get_all_level_actors():
        if isinstance(actor, unreal.Landscape):
            actor.set_editor_property("landscape_material", material)
            applied += 1
    unreal.log(f"Applied Ground Zero terrain material to {applied} landscape actor(s).")
    return applied


def load_heightmap():
    raw = HEIGHTMAP_PATH.read_bytes()
    expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
    if len(raw) != expected_bytes:
        raise RuntimeError(f"Unexpected heightmap size: {len(raw)} != {expected_bytes}")
    return struct.unpack(f"<{LANDSCAPE_SIZE * LANDSCAPE_SIZE}H", raw)


def terrain_z_cm(height_values, x_cm, y_cm):
    sample_x = max(0, min(LANDSCAPE_SIZE - 1, round((x_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
    sample_y = max(0, min(LANDSCAPE_SIZE - 1, round((y_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
    height_value = height_values[int(sample_y) * LANDSCAPE_SIZE + int(sample_x)]
    elevation_m = (float(height_value) - 32768.0) * Z_SCALE_CM / (100.0 * 128.0)
    return elevation_m * 100.0


def terrain_elevation_m(height_values, x_cm, y_cm):
    return terrain_z_cm(height_values, x_cm, y_cm) / 100.0


def terrain_slope_degrees(height_values, x_cm, y_cm, sample_distance_cm):
    dz_dx_m = (
        terrain_elevation_m(height_values, x_cm + sample_distance_cm, y_cm)
        - terrain_elevation_m(height_values, x_cm - sample_distance_cm, y_cm)
    ) / ((sample_distance_cm * 2.0) / 100.0)
    dz_dy_m = (
        terrain_elevation_m(height_values, x_cm, y_cm + sample_distance_cm)
        - terrain_elevation_m(height_values, x_cm, y_cm - sample_distance_cm)
    ) / ((sample_distance_cm * 2.0) / 100.0)
    return math.degrees(math.atan(math.sqrt((dz_dx_m * dz_dx_m) + (dz_dy_m * dz_dy_m))))


def remove_existing_demo_actors(labels):
    for actor in unreal.EditorLevelLibrary.get_all_level_actors():
        if get_actor_label(actor) in labels:
            unreal.EditorLevelLibrary.destroy_actor(actor)


def distance_2d(a, b):
    return math.hypot(a.x - b.x, a.y - b.y)


def validate_safe_spawn_location(height_values, location_xy, resource_points, water_points):
    failures = []
    elevation_m = terrain_elevation_m(height_values, location_xy.x, location_xy.y)
    slope_degrees = terrain_slope_degrees(
        height_values,
        location_xy.x,
        location_xy.y,
        SAFE_SPAWN_CONFIG["slope_sample_distance_cm"],
    )
    min_resource_distance_cm = min(distance_2d(location_xy, point) for point in resource_points)
    min_water_distance_cm = min(distance_2d(location_xy, point) for point in water_points)

    if elevation_m < SAFE_SPAWN_CONFIG["min_elevation_m"]:
        failures.append(f"elevation {elevation_m:.2f}m below minimum")
    if slope_degrees > SAFE_SPAWN_CONFIG["max_slope_degrees"]:
        failures.append(f"slope {slope_degrees:.2f}deg above maximum")
    if min_resource_distance_cm < SAFE_SPAWN_CONFIG["min_resource_distance_cm"]:
        failures.append(f"resource distance {min_resource_distance_cm:.0f}cm below minimum")
    if min_water_distance_cm < SAFE_SPAWN_CONFIG["min_water_distance_cm"]:
        failures.append(f"water distance {min_water_distance_cm:.0f}cm below minimum")

    return failures


def select_safe_spawn_location(height_values):
    resource_points = [spec["location_xy"] for spec in BIOME_RESOURCE_ACTORS]
    resource_points.extend(
        spec["location_xy"]
        for spec in DEMO_ACTORS
        if spec["label"] in {"AGR_DemoWoodResource_01", "AGR_DemoFiberResource_01"}
    )
    water_points = [spec["location_xy"] for spec in WATER_SOURCE_ACTORS]

    candidates = list(SAFE_SPAWN_CONFIG["candidate_locations_xy"])
    fallback = SAFE_SPAWN_CONFIG["fallback_location_xy"]
    if all(distance_2d(fallback, candidate) > 1.0 for candidate in candidates):
        candidates.append(fallback)

    rejected = []
    for candidate in candidates:
        failures = validate_safe_spawn_location(height_values, candidate, resource_points, water_points)
        if not failures:
            unreal.log(
                "Selected safe Ground Zero spawn at "
                f"({candidate.x:.0f}, {candidate.y:.0f}) "
                f"elevation {terrain_elevation_m(height_values, candidate.x, candidate.y):.2f}m "
                f"slope {terrain_slope_degrees(height_values, candidate.x, candidate.y, SAFE_SPAWN_CONFIG['slope_sample_distance_cm']):.2f}deg"
            )
            return candidate
        rejected.append(f"({candidate.x:.0f}, {candidate.y:.0f}): {', '.join(failures)}")

    raise RuntimeError("No safe Ground Zero spawn candidate found. Rejected: " + "; ".join(rejected))


def make_foliage_transform(height_values, x, y, yaw, z_offset, scale_xy, scale_z):
    return unreal.Transform(
        location=unreal.Vector(x, y, terrain_z_cm(height_values, x, y) + z_offset),
        rotation=unreal.Rotator(0.0, yaw, 0.0),
        scale=unreal.Vector(scale_xy, scale_xy, scale_z),
    )


def configure_foliage_meshes(foliage_actor):
    component_meshes = {
        "tree_instances": load_required_asset(FOLIAGE_MESHES["tree"]),
        "shrub_instances": load_required_asset(FOLIAGE_MESHES["shrub"]),
        "grass_instances": load_required_asset(FOLIAGE_MESHES["grass"]),
    }

    for property_name, mesh in component_meshes.items():
        component = foliage_actor.get_editor_property(property_name)
        component.set_editor_property("static_mesh", mesh)


def apply_foliage_materials(foliage_actor, materials):
    component_materials = {
        "tree_instances": materials["tree"],
        "shrub_instances": materials["shrub"],
        "grass_instances": materials["grass"],
    }

    for property_name, material in component_materials.items():
        component = foliage_actor.get_editor_property(property_name)
        component.set_material(0, material)


def choose_foliage_points(height_values, zone, reserved_points, existing_points):
    rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"]))
    chosen = []
    attempts = 0
    max_attempts = zone["count"] * 80

    while len(chosen) < zone["count"] and attempts < max_attempts:
        attempts += 1
        x = rng.uniform(zone["x_range"][0], zone["x_range"][1])
        y = rng.uniform(zone["y_range"][0], zone["y_range"][1])
        point = unreal.Vector(x, y, 0.0)

        if terrain_elevation_m(height_values, x, y) < zone["min_elevation_m"]:
            continue

        if any(distance_2d(point, reserved) < zone["avoid_radius_cm"] for reserved in reserved_points):
            continue

        if any(distance_2d(point, existing) < zone["avoid_radius_cm"] * 0.55 for existing in existing_points):
            continue

        chosen.append(point)
        existing_points.append(point)

    if len(chosen) != zone["count"]:
        unreal.log_warning(f"Placed {len(chosen)} of {zone['count']} requested foliage instances for zone.")

    return chosen


def spawn_foliage_actor(height_values, materials):
    reserved_points = [
        spec["location_xy"]
        for spec in DEMO_ACTORS
        if spec["label"] not in {"AGR_DemoSkyLightingController", "AGR_DemoWeatherAudioController", "AGR_DemoNoticeActor"}
    ]

    foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
        unreal.AgrarianFoliagePatch,
        unreal.Vector(0.0, 0.0, 0.0),
        unreal.Rotator(0.0, 0.0, 0.0),
        FOLIAGE_LABEL,
    )
    if not foliage_actor:
        raise RuntimeError("Could not spawn first-pass foliage actor.")

    set_actor_label(foliage_actor, FOLIAGE_LABEL)
    configure_foliage_meshes(foliage_actor)
    apply_foliage_materials(foliage_actor, materials)
    foliage_actor.clear_foliage()

    existing_points = []
    rng = random.Random(FOLIAGE_RANDOM_SEED)

    for point in choose_foliage_points(height_values, FOLIAGE_ZONES["trees"], reserved_points, existing_points):
        scale = rng.uniform(*FOLIAGE_ZONES["trees"]["scale_range"])
        foliage_actor.add_tree_instance(
            make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 5.0, scale, scale * 5.5)
        )

    for point in choose_foliage_points(height_values, FOLIAGE_ZONES["shrubs"], reserved_points, existing_points):
        scale = rng.uniform(*FOLIAGE_ZONES["shrubs"]["scale_range"])
        foliage_actor.add_shrub_instance(
            make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 4.0, scale * 2.2, scale * 0.85)
        )

    for point in choose_foliage_points(height_values, FOLIAGE_ZONES["grass"], reserved_points, existing_points):
        scale = rng.uniform(*FOLIAGE_ZONES["grass"]["scale_range"])
        foliage_actor.add_grass_instance(
            make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 2.0, scale * 0.28, scale * 1.6)
        )

    unreal.log(
        "Placed first-pass Ground Zero foliage: "
        f"{foliage_actor.get_tree_instance_count()} trees, "
        f"{foliage_actor.get_shrub_instance_count()} shrubs, "
        f"{foliage_actor.get_grass_instance_count()} grass clumps."
    )
    return foliage_actor


def spawn_demo_actor(spec, height_values, materials, safe_spawn_location_xy=None):
    location_xy = spec["location_xy"]
    if spec["label"] == SAFE_SPAWN_CONFIG["player_start_label"] and safe_spawn_location_xy is not None:
        location_xy = safe_spawn_location_xy

    z = spec.get("fixed_z")
    if z is None:
        z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)

    actor_class = spec.get("class")
    if actor_class is None:
        actor_class = load_blueprint_class(spec["class_path"])

    actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
        actor_class,
        unreal.Vector(location_xy.x, location_xy.y, z),
        spec["rotation"],
        spec["label"],
    )
    if not actor:
        raise RuntimeError(f"Could not spawn {spec['label']}")

    set_actor_label(actor, spec["label"])
    try:
        actor.set_editor_property("persistence_node_id", spec["label"])
    except Exception:
        pass
    material_key = material_key_for_actor_label(spec["label"])
    if material_key:
        apply_material_to_actor_meshes(actor, materials[material_key])
    unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}")
    return actor


def spawn_environment_variation_actor(spec, height_values, materials):
    location_xy = spec["location_xy"]
    z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
    actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
        unreal.StaticMeshActor,
        unreal.Vector(location_xy.x, location_xy.y, z),
        spec["rotation"],
        spec["label"],
    )
    if not actor:
        raise RuntimeError(f"Could not spawn {spec['label']}")

    set_actor_label(actor, spec["label"])
    actor.set_actor_scale3d(spec["scale"])
    mesh_component = actor.static_mesh_component
    mesh_component.set_editor_property("static_mesh", load_required_asset(VARIATION_MESHES[spec["mesh_key"]]))
    mesh_component.set_material(0, materials[spec["material_key"]])
    unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}")
    return actor


def spawn_weather_exposure_zone(spec, height_values):
    location_xy = spec["location_xy"]
    z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
    actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
        unreal.AgrarianWeatherExposureZone,
        unreal.Vector(location_xy.x, location_xy.y, z),
        unreal.Rotator(0.0, 0.0, 0.0),
        spec["label"],
    )
    if not actor:
        raise RuntimeError(f"Could not spawn {spec['label']}")

    set_actor_label(actor, spec["label"])
    actor.set_editor_property("exposure_zone_id", spec["zone_id"])
    actor.set_editor_property("exposure_multiplier", spec["exposure_multiplier"])
    actor.set_editor_property("temperature_offset_c", spec["temperature_offset_c"])
    actor.exposure_volume.set_box_extent(spec["extent"], True)
    unreal.log(
        f"Placed {spec['label']} at {actor.get_actor_location()} "
        f"exposure x{spec['exposure_multiplier']} temp {spec['temperature_offset_c']} C"
    )
    return actor


def spawn_map_boundary_volume():
    actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
        unreal.AgrarianMapBoundaryVolume,
        MAP_BOUNDARY_CONFIG["location"],
        unreal.Rotator(0.0, 0.0, 0.0),
        MAP_BOUNDARY_CONFIG["label"],
    )
    if not actor:
        raise RuntimeError(f"Could not spawn {MAP_BOUNDARY_CONFIG['label']}")

    set_actor_label(actor, MAP_BOUNDARY_CONFIG["label"])
    actor.set_editor_property("boundary_id", MAP_BOUNDARY_CONFIG["boundary_id"])
    actor.set_editor_property("boundary_padding_cm", MAP_BOUNDARY_CONFIG["padding_cm"])
    actor.set_editor_property("warning_distance_cm", MAP_BOUNDARY_CONFIG["warning_distance_cm"])
    actor.set_editor_property("clamp_players_at_boundary", True)
    actor.boundary_volume.set_box_extent(MAP_BOUNDARY_CONFIG["extent"], True)
    unreal.log(
        f"Placed {MAP_BOUNDARY_CONFIG['label']} at {actor.get_actor_location()} "
        f"extent {MAP_BOUNDARY_CONFIG['extent']}"
    )
    return actor


def main():
    if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
        raise RuntimeError(f"Could not load map: {MAP_PATH}")

    ensure_native_placeholder_meshes()

    labels = {spec["label"] for spec in DEMO_ACTORS}
    labels.update(LEGACY_DEMO_LIGHTING_LABELS)
    labels.update(spec["label"] for spec in BIOME_RESOURCE_ACTORS)
    labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS)
    labels.update(spec["label"] for spec in WEATHER_EXPOSURE_ZONES)
    labels.update(spec["label"] for spec in ENVIRONMENT_VARIATION_ACTORS)
    labels.update(spec["label"] for spec in RUIN_PLACEHOLDER_ACTORS)
    labels.add(FOLIAGE_LABEL)
    labels.add(MAP_BOUNDARY_CONFIG["label"])
    remove_existing_demo_actors(labels)

    materials = ensure_environment_materials()
    apply_landscape_material(materials["terrain"])
    height_values = load_heightmap()
    safe_spawn_location_xy = select_safe_spawn_location(height_values)
    spawn_foliage_actor(height_values, materials)
    for spec in BIOME_RESOURCE_ACTORS:
        spawn_demo_actor(spec, height_values, materials)
    for spec in WATER_SOURCE_ACTORS:
        spawn_demo_actor(spec, height_values, materials)
    for spec in WEATHER_EXPOSURE_ZONES:
        spawn_weather_exposure_zone(spec, height_values)
    for spec in ENVIRONMENT_VARIATION_ACTORS:
        spawn_environment_variation_actor(spec, height_values, materials)
    for spec in RUIN_PLACEHOLDER_ACTORS:
        spawn_environment_variation_actor(spec, height_values, materials)
    spawn_map_boundary_volume()
    for spec in DEMO_ACTORS:
        spawn_demo_actor(spec, height_values, materials, safe_spawn_location_xy)

    unreal.EditorLevelLibrary.save_current_level()
    unreal.log("Ground Zero demo map setup complete.")


main()
