//
//  Path.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace NosSmooth.Extensions.Pathfinding;

/// <summary>
/// Represents a found walkable path.
/// </summary>
public class Path
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Path"/> class.
    /// </summary>
    /// <param name="map">The map id.</param>
    /// <param name="x">The current x.</param>
    /// <param name="y">The current y.</param>
    /// <param name="targetX">The target x.</param>
    /// <param name="targetY">The target y.</param>
    /// <param name="parts">The parts that represent the path from the current to the target.</param>
    public Path
    (
        int map,
        short x,
        short y,
        short targetX,
        short targetY,
        IReadOnlyList<(short X, short Y)> parts
    )
    {
        MapId = map;
        CurrentX = x;
        CurrentY = y;
        TargetX = targetX;
        TargetY = targetY;
        Parts = parts;
        CurrentPartIndex = 0;
    }

    /// <summary>
    /// Gets the map id this path is for.
    /// </summary>
    public int MapId { get; }

    /// <summary>
    /// Gets whether the path has reached an end.
    /// </summary>
    public bool ReachedEnd => CurrentPartIndex >= Parts.Count - 1;

    /// <summary>
    /// Gets the current walk path index.
    /// </summary>
    public int CurrentPartIndex { get; private set; }

    /// <summary>
    /// Gets the list of the parts that have to be taken.
    /// </summary>
    public IReadOnlyList<(short X, short Y)> Parts { get; }

    /// <summary>
    /// Gets the target x coordinate.
    /// </summary>
    public short TargetX { get; }

    /// <summary>
    /// Gets the target y coordinate.
    /// </summary>
    public short TargetY { get; }

    /// <summary>
    /// Gets the current x coordinate.
    /// </summary>
    public short CurrentX { get; private set; }

    /// <summary>
    /// gets the current y coordinate.
    /// </summary>
    public short CurrentY { get; private set; }

    /// <summary>
    /// Take a path only in the same direction.
    /// </summary>
    /// <returns>A position to walk to.</returns>
    public (short X, short Y) TakeForwardPath()
    {
        if (ReachedEnd)
        {
            return (TargetX, TargetY);
        }
        if (CurrentPartIndex + 2 >= Parts.Count)
        {
            CurrentPartIndex++;
            return (TargetX, TargetY);
        }

        var zeroTile = (CurrentX, CurrentY);
        var firstTile = Parts[++CurrentPartIndex];
        var currentTile = firstTile;
        var nextTile = Parts[CurrentPartIndex + 1];

        while (!ReachedEnd && IsInLine(zeroTile, firstTile, nextTile))
        {
            currentTile = nextTile;
            CurrentPartIndex++;
            if (!ReachedEnd)
            {
                nextTile = Parts[CurrentPartIndex + 1];
            }
        }

        return currentTile;
    }

    private bool IsInLine((short X, short Y) start, (short X, short Y) first, (short X, short Y) current)
    {
        var xFirstDiff = first.X - start.X;
        var yFirstDiff = first.Y - start.Y;

        var xCurrentDiff = current.X - start.X;
        var yCurrentDiff = current.Y - start.Y;

        if (xFirstDiff == 0 && yFirstDiff == 0)
        {
            throw new ArgumentException("The path went back to the start.");
        }

        if (xCurrentDiff == 0 && yCurrentDiff == 0)
        {
            throw new ArgumentException("The path went back to the start.");
        }

        if (xFirstDiff != 0)
        {
            var xRatio = xCurrentDiff / (float)xFirstDiff;
            return Math.Abs((yFirstDiff * xRatio) - yCurrentDiff) < float.Epsilon;
        }

        var yRatio = yCurrentDiff / (float)yFirstDiff;
        return Math.Abs((xFirstDiff * yRatio) - xCurrentDiff) < float.Epsilon;
    }

    /// <summary>
    /// Take the given number of tiles and return the position we ended up at.
    /// </summary>
    /// <remarks>
    /// If the count is greater than what is remaining, the end will be taken.
    /// </remarks>
    /// <param name="partsCount">The count of parts to take.</param>
    /// <returns>A position to walk to.</returns>
    public (short X, short Y) TakePath(uint partsCount)
    {
        if (ReachedEnd)
        {
            return (TargetX, TargetY);
        }

        if (CurrentPartIndex + partsCount >= Parts.Count - 1)
        {
            CurrentPartIndex = Parts.Count - 1;
        }
        else
        {
            CurrentPartIndex += (int)partsCount;
        }

        return Parts[CurrentPartIndex];
    }
}