using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Theriapolis.Core;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Time;
using Theriapolis.Core.Util;
using Theriapolis.Core.World;
using Theriapolis.Game.Rendering;

namespace Theriapolis.Game.Input;

/// <summary>
/// Drives the player. World-map mode: click a destination, A* the path,
/// and animate the player along it while the WorldClock advances. Tactical
/// step input is added in M3 once the chunk streamer is in place — until then,
/// tactical mode is a passive observer (zoom and look around).
/// </summary>
public sealed class PlayerController
{
    private readonly PlayerActor _player;
    private readonly WorldState _world;
    private readonly WorldClock _clock;
    private readonly WorldTravelPlanner _planner;

    /// <summary>
    /// Optional callback installed by PlayScreen once tactical streaming is up
    /// (M3+). Returns whether the given tactical-tile coord is walkable.
    /// </summary>
    public Func<int, int, bool>? TacticalIsWalkable { get; set; }

    private List<(int X, int Y)>? _path;       // tile waypoints
    private int _pathIndex;                    // index of the next waypoint

    // Sub-second carry for the world clock — tactical motion is continuous,
    // so a single frame may advance fewer than one in-game second; without
    // this carry, slow movement would never tick the clock past 0.
    private float _tacticalClockCarry;

    public bool IsTraveling => _path is not null && _pathIndex < _path.Count;

    public PlayerController(PlayerActor player, WorldState world, WorldClock clock)
    {
        _player  = player;
        _world   = world;
        _clock   = clock;
        _planner = new WorldTravelPlanner(world);
    }

    public void CancelTravel()
    {
        _path = null;
        _pathIndex = 0;
    }

    /// <summary>Queue a click destination as a new travel plan. Returns true if a path was found.</summary>
    public bool RequestTravelTo(int tileX, int tileY)
    {
        int sx = (int)MathF.Floor(_player.Position.X / C.WORLD_TILE_PIXELS);
        int sy = (int)MathF.Floor(_player.Position.Y / C.WORLD_TILE_PIXELS);
        sx = Math.Clamp(sx, 0, C.WORLD_WIDTH_TILES  - 1);
        sy = Math.Clamp(sy, 0, C.WORLD_HEIGHT_TILES - 1);

        var path = _planner.PlanTilePath(sx, sy, tileX, tileY);
        if (path is null || path.Count < 2) return false;
        _path = path;
        _pathIndex = 1; // we're already at the start tile
        return true;
    }

    public void Update(GameTime gt, InputManager input, Camera2D camera, bool isWindowFocused)
    {
        float dt = (float)gt.ElapsedGameTime.TotalSeconds;
        if (camera.Mode == ViewMode.WorldMap)
            UpdateWorldMap(dt);
        else
            UpdateTactical(dt, input, isWindowFocused);
    }

    private void UpdateWorldMap(float dt)
    {
        if (_path is null) return;
        if (_pathIndex >= _path.Count) { _path = null; return; }

        var (tx, ty) = _path[_pathIndex];
        var target   = WorldTravelPlanner.TileCenterToWorldPixel(tx, ty);
        var curPos   = _player.Position;
        var diff     = target - curPos;
        float dist   = diff.Length;
        float move   = _player.SpeedWorldPxPerSec * dt;

        if (move >= dist)
        {
            int prevTileX = (int)MathF.Floor(curPos.X / C.WORLD_TILE_PIXELS);
            int prevTileY = (int)MathF.Floor(curPos.Y / C.WORLD_TILE_PIXELS);
            _player.Position = target;
            if (dist > 1e-3f) _player.FacingAngleRad = MathF.Atan2(diff.Y, diff.X);
            float legSeconds = _planner.EstimateSecondsForLeg(prevTileX, prevTileY, tx, ty);
            _clock.Advance((long)MathF.Round(legSeconds));
            _pathIndex++;
            if (_pathIndex >= _path.Count) _path = null;
        }
        else
        {
            var step = diff.Normalized * move;
            _player.Position = curPos + step;
            _player.FacingAngleRad = MathF.Atan2(diff.Y, diff.X);
            ref var dst = ref _world.TileAt(tx, ty);
            float secondsThisFrame = move * _planner.SecondsPerPixel(dst);
            _clock.Advance((long)MathF.Round(secondsThisFrame));
        }
    }

    private void UpdateTactical(float dt, InputManager input, bool isWindowFocused)
    {
        // M3 will install TacticalIsWalkable; until then there's nothing to do.
        if (!isWindowFocused || TacticalIsWalkable is null) return;

        int dx = 0, dy = 0;
        if (input.IsDown(Keys.W) || input.IsDown(Keys.Up))    dy = -1;
        if (input.IsDown(Keys.S) || input.IsDown(Keys.Down))  dy = +1;
        if (input.IsDown(Keys.A) || input.IsDown(Keys.Left))  dx = -1;
        if (input.IsDown(Keys.D) || input.IsDown(Keys.Right)) dx = +1;
        if (dx == 0 && dy == 0) return;

        // Normalize so diagonal isn't √2 faster than cardinal.
        float invLen = (dx != 0 && dy != 0) ? 0.70710678f : 1f;
        float vx = dx * invLen;
        float vy = dy * invLen;

        // Phase 5 M3: apply encumbrance multiplier when a Character is attached.
        // Carrying ≤ 100% of capacity walks at full speed; >100% is heavy
        // (×0.66); >150% is over-encumbered (×0.50).
        float encMult = _player.Character is not null
            ? Theriapolis.Core.Rules.Stats.DerivedStats.TacticalSpeedMult(_player.Character)
            : 1f;
        float speed  = C.TACTICAL_PLAYER_PX_PER_SEC * encMult;
        float moveX  = vx * speed * dt;
        float moveY  = vy * speed * dt;

        var pos = _player.Position;

        // Axis-separated motion gives wall-sliding for free: if X is blocked,
        // Y still moves, and vice versa. Each axis tests the destination tile
        // (with a small body radius so the player doesn't visibly clip walls).
        const float BodyRadius = 0.35f; // tactical tiles
        float newX = pos.X + moveX;
        if (CanOccupy(newX, pos.Y, BodyRadius)) pos = new Vec2(newX, pos.Y);
        float newY = pos.Y + moveY;
        if (CanOccupy(pos.X, newY, BodyRadius)) pos = new Vec2(pos.X, newY);

        _player.Position = pos;
        _player.FacingAngleRad = MathF.Atan2(vy, vx);

        // Clock: 1 tactical pixel walked = TACTICAL_STEP_SECONDS in-game seconds.
        // Sub-second motion accumulates in _tacticalClockCarry so slow walking
        // still ticks the clock cumulatively.
        float walked = MathF.Sqrt(moveX * moveX + moveY * moveY);
        float secondsThisFrame = walked * C.TACTICAL_STEP_SECONDS + _tacticalClockCarry;
        long whole = (long)MathF.Floor(secondsThisFrame);
        _tacticalClockCarry = secondsThisFrame - whole;
        if (whole > 0) _clock.Advance(whole);
    }

    private bool CanOccupy(float x, float y, float r)
    {
        // Sample the four corners of the player's body AABB so we don't slip
        // into walls when sliding past corners.
        return TacticalIsWalkable!((int)MathF.Floor(x - r), (int)MathF.Floor(y - r))
            && TacticalIsWalkable!((int)MathF.Floor(x + r), (int)MathF.Floor(y - r))
            && TacticalIsWalkable!((int)MathF.Floor(x - r), (int)MathF.Floor(y + r))
            && TacticalIsWalkable!((int)MathF.Floor(x + r), (int)MathF.Floor(y + r));
    }
}
