namespace Snake.Core;

public sealed class Snake
{
    private readonly LinkedList<Position> _segments = new();
    private readonly Queue<Direction> _pendingDirections = new();

    public Snake(Position start, int length, Direction direction)
    {
        if (length < 1)
            throw new ArgumentOutOfRangeException(nameof(length), "Length must be at least 1.");

        Direction = direction;

        var offset = DirectionToOffset(direction);
        for (var i = 0; i < length; i++)
        {
            var pos = new Position(
                start.X - offset.X * i,
                start.Y - offset.Y * i);
            _segments.AddLast(pos);
            _previousSegments.Add(pos);
        }
    }

    private readonly List<Position> _previousSegments = new();

    public Direction Direction { get; private set; }

    public Position Head => _segments.First!.Value;

    public IReadOnlyCollection<Position> Segments => _segments;

    /// <summary>
    /// Segment positions before the last Move() call, used for interpolation.
    /// Empty (Count == 0) before first move.
    /// </summary>
    public IReadOnlyList<Position> PreviousSegments => _previousSegments;

    /// <summary>
    /// Enqueues a direction change to be applied on a future tick.
    /// </summary>
    public void SetDirection(Direction direction)
    {
        _pendingDirections.Enqueue(direction);
    }

    /// <summary>
    /// Returns the head position after the next move without changing state.
    /// Invalid queued directions (same as current or opposite) are discarded.
    /// </summary>
    public Position PeekNextHead()
    {
        DiscardInvalidPendingDirections();
        var direction = _pendingDirections.Count > 0 ? _pendingDirections.Peek() : Direction;
        var offset = DirectionToOffset(direction);
        return new Position(Head.X + offset.X, Head.Y + offset.Y);
    }

    /// <summary>
    /// Applies at most one valid queued direction change, then moves the snake.
    /// </summary>
    public Position Move(bool grow = false)
    {
        DiscardInvalidPendingDirections();
        if (_pendingDirections.Count > 0)
            Direction = _pendingDirections.Dequeue();

        // Capture current segments as "previous" for interpolation
        _previousSegments.Clear();
        foreach (var seg in _segments)
            _previousSegments.Add(seg);

        var offset = DirectionToOffset(Direction);
        var newHead = new Position(Head.X + offset.X, Head.Y + offset.Y);

        _segments.AddFirst(newHead);

        if (!grow)
            _segments.RemoveLast();

        return newHead;
    }

    public bool Occupies(Position position, bool excludeTail = false)
    {
        if (excludeTail && _segments.Last is { } last && last.Value == position)
            return false;

        return _segments.Contains(position);
    }

    /// <summary>
    /// Removes queued directions that would turn backwards or repeat the current direction.
    /// </summary>
    private void DiscardInvalidPendingDirections()
    {
        while (_pendingDirections.Count > 0)
        {
            var next = _pendingDirections.Peek();
            if (next != Direction && !AreOpposite(Direction, next))
                break;

            _pendingDirections.Dequeue();
        }
    }

    private static Position DirectionToOffset(Direction direction) =>
        direction switch
        {
            Direction.Up => new Position(0, -1),
            Direction.Down => new Position(0, 1),
            Direction.Left => new Position(-1, 0),
            Direction.Right => new Position(1, 0),
            _ => throw new ArgumentOutOfRangeException(nameof(direction))
        };

    private static bool AreOpposite(Direction current, Direction next) =>
        (current, next) switch
        {
            (Direction.Up, Direction.Down) => true,
            (Direction.Down, Direction.Up) => true,
            (Direction.Left, Direction.Right) => true,
            (Direction.Right, Direction.Left) => true,
            _ => false
        };
}
