import math
from pathlib import Path

import unreal


MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
EXPECTED_FOLIAGE_COUNTS = {
    "trees": 64,
    "shrubs": 148,
    "grass": 260,
}
CRITICAL_CLEARANCE_CM = {
    "trees": 5200.0,
    "shrubs": 3400.0,
    "grass": 1250.0,
}
CORRIDOR_CLEARANCE_CM = {
    "trees": 3600.0,
    "shrubs": 2100.0,
    "grass": 850.0,
}
CORRIDOR_SAMPLE_SPACING_CM = 1600.0
CRITICAL_ACTOR_LABELS = {
    "AGR_DemoPlayerStart",
    "AGR_DemoWoodResource_01",
    "AGR_DemoFiberResource_01",
    "AGR_DemoCampfire_01",
    "AGR_DemoPrimitiveShelter_01",
    "AGR_DemoRabbitWildlife_01",
    "AGR_GZ_FreshWaterSource_01",
    "AGR_GZ_Wood_CoastalScrub_01",
    "AGR_GZ_Wood_CoastalScrub_02",
    "AGR_GZ_Wood_Hillside_03",
    "AGR_GZ_Fiber_Grassland_01",
    "AGR_GZ_Fiber_Grassland_02",
    "AGR_GZ_Fiber_Scrub_03",
    "AGR_GZ_Fiber_DrainageCandidate_04",
    "AGR_GZ_EdiblePlant_CoastalScrub_01",
    "AGR_GZ_EdiblePlant_Grassland_02",
    "AGR_GZ_EdiblePlant_DrainageCandidate_03",
    "AGR_GZ_Stone_Slope_01",
    "AGR_GZ_Stone_Slope_02",
    "AGR_GZ_Stone_ExposedTerrain_03",
    "AGR_GZ_Stone_ValleyEdge_04",
}
SIGHTLINE_PAIRS = [
    ("AGR_DemoPlayerStart", "AGR_DemoWoodResource_01"),
    ("AGR_DemoPlayerStart", "AGR_DemoFiberResource_01"),
    ("AGR_DemoPlayerStart", "AGR_DemoCampfire_01"),
    ("AGR_DemoPlayerStart", "AGR_DemoPrimitiveShelter_01"),
    ("AGR_DemoPlayerStart", "AGR_DemoRabbitWildlife_01"),
    ("AGR_DemoPlayerStart", "AGR_GZ_FreshWaterSource_01"),
]


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


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


def transform_location(transform_result):
    transform = transform_result[0] if isinstance(transform_result, tuple) else transform_result
    if hasattr(transform, "translation"):
        return transform.translation
    try:
        return transform.get_editor_property("translation")
    except Exception:
        return transform.get_location()


def instance_locations(component):
    locations = []
    for index in range(component.get_instance_count()):
        locations.append(transform_location(component.get_instance_transform(index, True)))
    return locations


def sampled_corridor_points(start, end):
    length_cm = distance_2d(start, end)
    sample_count = max(1, int(length_cm / CORRIDOR_SAMPLE_SPACING_CM))
    points = []
    for index in range(1, sample_count):
        alpha = index / sample_count
        points.append(
            unreal.Vector(
                start.x + ((end.x - start.x) * alpha),
                start.y + ((end.y - start.y) * alpha),
                0.0,
            )
        )
    return points


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

    actors = unreal.EditorLevelLibrary.get_all_level_actors()
    actors_by_label = {get_actor_label(actor): actor for actor in actors}
    failures = []

    missing_labels = sorted(label for label in CRITICAL_ACTOR_LABELS if label not in actors_by_label)
    if missing_labels:
        failures.append("missing critical actor label(s): " + ", ".join(missing_labels))

    foliage = actors_by_label.get(FOLIAGE_LABEL)
    if not foliage:
        failures.append(f"missing {FOLIAGE_LABEL}")
    else:
        foliage_components = {
            "trees": foliage.get_editor_property("tree_instances"),
            "shrubs": foliage.get_editor_property("shrub_instances"),
            "grass": foliage.get_editor_property("grass_instances"),
        }
        foliage_locations = {
            family: instance_locations(component)
            for family, component in foliage_components.items()
        }

        for family, expected_count in EXPECTED_FOLIAGE_COUNTS.items():
            actual_count = len(foliage_locations[family])
            if actual_count != expected_count:
                failures.append(f"{family} expected {expected_count}, got {actual_count}")

        critical_points = {
            label: actors_by_label[label].get_actor_location()
            for label in CRITICAL_ACTOR_LABELS
            if label in actors_by_label
        }
        for family, locations in foliage_locations.items():
            clearance = CRITICAL_CLEARANCE_CM[family]
            for label, point in critical_points.items():
                nearest = min((distance_2d(location, point) for location in locations), default=999999.0)
                if nearest < clearance:
                    failures.append(
                        f"{family} nearest to {label} is {nearest:.0f}cm, below {clearance:.0f}cm clearance"
                    )

        for start_label, end_label in SIGHTLINE_PAIRS:
            start_actor = actors_by_label.get(start_label)
            end_actor = actors_by_label.get(end_label)
            if not start_actor or not end_actor:
                continue

            corridor_points = sampled_corridor_points(
                start_actor.get_actor_location(),
                end_actor.get_actor_location(),
            )
            for family, locations in foliage_locations.items():
                clearance = CORRIDOR_CLEARANCE_CM[family]
                for point in corridor_points:
                    nearest = min((distance_2d(location, point) for location in locations), default=999999.0)
                    if nearest < clearance:
                        failures.append(
                            f"{family} nearest to {start_label}->{end_label} corridor is "
                            f"{nearest:.0f}cm, below {clearance:.0f}cm clearance"
                        )
                        break

    project_root = Path(unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()))
    docs = project_root / "Docs" / "Terrain" / "GroundZeroDensitySightlineTuning.md"
    roadmap = project_root / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
    for path, snippets in {
        docs: ["Ground Zero Density And Sightline Tuning", "First-Look Sightlines"],
        roadmap: ["[x] Add density and sightline tuning"],
    }.items():
        text = path.read_text(encoding="utf-8")
        for snippet in snippets:
            if snippet not in text:
                failures.append(f"{path} missing snippet: {snippet}")

    if failures:
        raise RuntimeError("Ground Zero density/sightline verification failed: " + "; ".join(failures))

    unreal.log(
        "Ground Zero density/sightline verification complete: "
        f"{len(CRITICAL_ACTOR_LABELS)} critical labels, "
        f"{len(SIGHTLINE_PAIRS)} sightline corridor(s), "
        f"{EXPECTED_FOLIAGE_COUNTS['trees']} trees, "
        f"{EXPECTED_FOLIAGE_COUNTS['shrubs']} shrubs, "
        f"{EXPECTED_FOLIAGE_COUNTS['grass']} grass clumps."
    )


main()
