using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Graphics;
using MrGameEng.Input;

namespace LittleSim.Sim;

/// <summary>Житель бродит: направление меняется по таймеру, движение детерминировано сидом.</summary>
public struct Wander : IComponent
{
    public Vector2 Direction;
    public float ChangeIn;
}

/// <summary>Имя дефа жителя — нужно сохранению, чтобы при загрузке пересобрать спрайт из атласа.</summary>
public struct PawnId : IComponent
{
    public string DefName;
}

/// <summary>
/// Блуждание жителей. Один Random на систему, сид от мира — при одинаковом сиде
/// и порядке обновления симуляция воспроизводима. Отдыхающие жители (см.
/// <see cref="PawnDecisionSystem"/>) стоят на месте — таймер смены направления тоже замирает.
/// </summary>
public sealed class WanderSystem(int seed, RectF worldBounds)
    : QuerySystem<Transform2D, Wander, PawnBrain>
{
    private const float Speed = 22f;

    private readonly Random _random = new(seed);

    protected override void OnUpdate()
    {
        var delta = Tick.deltaTime;
        foreach (var (transforms, wanders, brains, _) in Query.Chunks)
        {
            var t = transforms.Span;
            var w = wanders.Span;
            var b = brains.Span;
            for (var i = 0; i < t.Length; i++)
            {
                if (b[i].Action == PawnAction.Rest)
                {
                    continue;
                }

                ref var wander = ref w[i];
                wander.ChangeIn -= delta;
                if (wander.ChangeIn <= 0f)
                {
                    var angle = _random.NextSingle() * MathF.Tau;
                    wander.Direction = new Vector2(MathF.Cos(angle), MathF.Sin(angle));
                    wander.ChangeIn = 1f + _random.NextSingle() * 3f;
                }

                ref var position = ref t[i].Position;
                position += wander.Direction * (Speed * delta);
                position.X = Math.Clamp(position.X, worldBounds.Left, worldBounds.Right);
                position.Y = Math.Clamp(position.Y, worldBounds.Top, worldBounds.Bottom);
            }
        }
    }
}

/// <summary>
/// Расталкивание пересёкшихся круговых коллайдеров по парам кадра: животные не
/// наслаиваются друг на друга и выталкиваются из статичных препятствий (деревьев).
/// Статичность определяется отсутствием компонента <see cref="Wander"/>.
/// Регистрировать после системы коллизий.
/// </summary>
public sealed class SeparationSystem(MrGameEng.Collisions.CollisionWorld world) : BaseSystem
{
    protected override void OnUpdateGroup()
    {
        foreach (var pair in world.Pairs)
        {
            ref var ta = ref pair.A.GetComponent<Transform2D>();
            ref var tb = ref pair.B.GetComponent<Transform2D>();
            var sum =
                pair.A.GetComponent<MrGameEng.Collisions.Collider>().Radius
                + pair.B.GetComponent<MrGameEng.Collisions.Collider>().Radius;

            var delta = tb.Position - ta.Position;
            var distance = delta.Length();
            var overlap = sum - distance;
            if (overlap <= 0f)
            {
                continue;
            }

            var direction = distance > 0.001f ? delta / distance : Vector2.UnitX;
            var aStatic = !pair.A.HasComponent<Wander>();
            var bStatic = !pair.B.HasComponent<Wander>();
            if (aStatic && bStatic)
            {
                continue;
            }

            if (aStatic)
            {
                tb.Position += direction * overlap;
            }
            else if (bStatic)
            {
                ta.Position -= direction * overlap;
            }
            else
            {
                ta.Position -= direction * (overlap * 0.5f);
                tb.Position += direction * (overlap * 0.5f);
            }
        }
    }
}

/// <summary>
/// Камера бога: WASD/стрелки — панорамирование, колесо — зум к курсору (мировая точка под мышью
/// остаётся под мышью). Молчит, пока открыта консоль или активен <paramref name="blocked"/>
/// (например, открыто меню-пауза).
/// </summary>
public sealed class GodCameraSystem(
    Entity cameraEntity,
    InputManager input,
    Renderer2D renderer,
    MrGameEng.DevConsole.DevConsole console,
    Func<bool>? blocked = null
) : BaseSystem
{
    protected override void OnUpdateGroup()
    {
        if (console.IsOpen || blocked?.Invoke() == true)
        {
            return;
        }

        ref var camera = ref cameraEntity.GetComponent<Camera>();

        var pan = new Vector2(
            Axis(input, Keys.A, Keys.Left, Keys.D, Keys.Right),
            Axis(input, Keys.W, Keys.Up, Keys.S, Keys.Down)
        );
        if (pan != Vector2.Zero)
        {
            pan.Normalize();
            camera.Position += pan * (500f / camera.Zoom) * Tick.deltaTime;
        }

        if (input.WheelDelta != 0)
        {
            var oldZoom = camera.Zoom;
            var newZoom = Math.Clamp(oldZoom * (1f + input.WheelDelta * 0.001f), 0.4f, 8f);
            if (newZoom != oldZoom)
            {
                // Точка мира под курсором до зума; после — сдвигаем камеру, чтобы она осталась там же.
                var anchor = renderer.ScreenToWorld(
                    new Vector2(input.MousePosition.X, input.MousePosition.Y)
                );
                camera.Zoom = newZoom;
                camera.Position = anchor - (anchor - camera.Position) * (oldZoom / newZoom);
            }
        }
    }

    private static float Axis(
        InputManager input,
        Keys negativeA,
        Keys negativeB,
        Keys positiveA,
        Keys positiveB
    ) =>
        (input.IsKeyDown(positiveA) || input.IsKeyDown(positiveB) ? 1f : 0f)
        - (input.IsKeyDown(negativeA) || input.IsKeyDown(negativeB) ? 1f : 0f);
}

/// <summary>
/// Строка статуса в HUD: заголовок сцены (ключ локализации с сидом, датой/временем календаря и
/// FPS) и подсказка управления. Обновляется 4 раза в секунду; смена языка подхватывается сама.
/// </summary>
public sealed class HudSystem(
    MrGameEng.Core.EngineContext context,
    MrGameEng.Mods.LanguageManager languages,
    Myra.Graphics2D.UI.Label label,
    string titleKey,
    int seed,
    MrGameEng.Core.Calendar? calendar = null,
    MrGameEng.Core.Climate? climate = null
) : BaseSystem
{
    private float _accumulated;
    private int _frames;

    protected override void OnUpdateGroup()
    {
        _accumulated += context.Clock.UnscaledDeltaTime;
        _frames++;
        if (_accumulated < 0.25f)
        {
            return;
        }

        var fps = (int)MathF.Round(_frames / _accumulated);
        _accumulated = 0f;
        _frames = 0;
        var when = calendar is null
            ? ""
            : languages.Format("hud.datetime", calendar.Day, calendar.Hour, calendar.Minute);
        if (climate is not null)
        {
            var season = languages.Get("season." + climate.Season.ToString().ToLowerInvariant());
            when +=
                " | "
                + languages.Format("hud.climate", season, (int)MathF.Round(climate.Temperature));
        }

        label.Text =
            languages.Format(titleKey, seed, when, fps) + "\n" + languages.Get("hud.controls");
    }
}
