namespace splat;

using Godot;
using System.Linq;

/// <summary>
/// Primary game scene root (3D). Wires together the camera, input, time, and
/// building/traffic systems, and handles placement/demolish interaction.
/// </summary>
public partial class Game : Node3D
{
    [Export] public PackedScene BuildingScene { get; set; } = default!;
    [Export] public PackedScene RoadScene { get; set; } = default!;

    public CameraController CameraRig { get; private set; } = default!;
    public InputManager Input { get; private set; } = default!;
    public GameManager Manager { get; private set; } = default!;
    public BuildingManager Buildings { get; private set; } = default!;
    public TrafficManager Traffic { get; private set; } = default!;
    public Terrain Ground { get; private set; } = default!;
    public HUD Hud { get; private set; } = default!;
    public BuildMenu BuildMenu { get; private set; } = default!;

    private Node3D _buildingRoot = default!;
    private Node3D _roadRoot = default!;

    public override void _Ready()
    {
        Manager = GetNode<GameManager>("GameManager");
        Input = GetNode<InputManager>("InputManager");
        Buildings = GetNode<BuildingManager>("BuildingManager");
        Traffic = GetNode<TrafficManager>("TrafficManager");
        Ground = GetNode<Terrain>("Terrain");
        CameraRig = GetNode<CameraController>("CameraRig");
        Hud = GetNode<HUD>("HUD");
        BuildMenu = GetNode<BuildMenu>("BuildMenu");
        _buildingRoot = GetNode<Node3D>("World/Buildings");
        _roadRoot = GetNode<Node3D>("World/Roads");

        Input.ToolChanged += OnToolChanged;
        Input.BuildMenuRequested += OnBuildMenuRequested;
        Input.CancelRequested += OnCancelRequested;
        BuildMenu.ToolSelected += OnToolSelectedFromMenu;

        foreach (var building in _buildingRoot.GetChildren().OfType<Building>())
            Buildings.Register(building);
        foreach (var road in _roadRoot.GetChildren().OfType<Road>())
            Traffic.RegisterRoad(road);
    }

    public override void _UnhandledInput(InputEvent @event)
    {
        if (BuildMenu.Visible)
            return;

        if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left } mouse)
        {
            ActivateToolAt(mouse.Position);
            return;
        }

        if (@event.IsActionPressed("placement_confirm"))
            ActivateToolAt(GetViewport().GetVisibleRect().Size / 2f);
    }

    private void ActivateToolAt(Vector2 screenPosition)
    {
        var tool = Input.CurrentTool;
        if (tool == InputManager.Tool.PlaceBuilding)
            TryPlaceBuilding(screenPosition);
        else if (tool == InputManager.Tool.PlaceRoad)
            TryPlaceRoad(screenPosition);
        else if (tool == InputManager.Tool.Demolish)
            TryDemolish(screenPosition);
    }

    private void TryPlaceBuilding(Vector2 screenPosition)
    {
        var hit = RaycastToGround(screenPosition);
        if (hit == null)
            return;

        if (BuildingScene == null)
            return;

        var building = BuildingScene.Instantiate<Building>();
        building.Data = (BuildingData)building.Data.Duplicate(true);
        building.Data.Init();
        building.Position = SnapToGrid(hit.Value, 2f);
        _buildingRoot.AddChild(building);
        Buildings.Register(building);
    }

    private void TryPlaceRoad(Vector2 screenPosition)
    {
        var hit = RaycastToGround(screenPosition);
        if (hit == null)
            return;

        if (RoadScene == null)
            return;

        var road = RoadScene.Instantiate<Road>();
        road.Data = (RoadData)road.Data.Duplicate(true);
        road.Position = SnapToGrid(hit.Value, 4f);
        _roadRoot.AddChild(road);
        Traffic.RegisterRoad(road);
    }

    private void TryDemolish(Vector2 screenPosition)
    {
        var camera = CameraRig.GetNode<Camera3D>("Camera3D");
        var from = camera.ProjectRayOrigin(screenPosition);
        var to = from + camera.ProjectRayNormal(screenPosition) * 1000f;

        var space = GetWorld3D().DirectSpaceState;
        var query = PhysicsRayQueryParameters3D.Create(from, to);
        query.CollisionMask = 1; // Buildings layer
        var result = space.IntersectRay(query);
        if (result.Count == 0 || result["collider"].Obj is not Building b)
            return;

        Buildings.Unregister(b);
        b.QueueFree();
    }

    private Vector3? RaycastToGround(Vector2 screenPos)
    {
        var camera = CameraRig.GetNode<Camera3D>("Camera3D");
        var from = camera.ProjectRayOrigin(screenPos);
        var to = from + camera.ProjectRayNormal(screenPos) * 1000f;

        var space = GetWorld3D().DirectSpaceState;
        var query = PhysicsRayQueryParameters3D.Create(from, to);
        query.CollisionMask = 16; // Terrain layer (layer 5)
        var result = space.IntersectRay(query);
        if (result.Count == 0)
            return null;

        return (Vector3)result["position"];
    }

    private void OnToolChanged(InputManager.Tool tool)
    {
        Hud.SetTool(tool);
    }

    private void OnBuildMenuRequested()
        => BuildMenu.Toggle();

    private void OnCancelRequested()
    {
        if (BuildMenu.Visible)
            BuildMenu.Close();
        else
            Input.SetTool(InputManager.Tool.Select);
    }

    private void OnToolSelectedFromMenu(InputManager.Tool tool)
    {
        Input.SetTool(tool);
        BuildMenu.Close();
    }

    private static Vector3 SnapToGrid(Vector3 position, float gridSize)
        => new(
            Mathf.Round(position.X / gridSize) * gridSize,
            position.Y,
            Mathf.Round(position.Z / gridSize) * gridSize
        );
}
