using UnityEngine;
using Vector3 = UnityEngine.Vector3;

public readonly struct ControlledPlayerCorrectionSettings
{
    public ControlledPlayerCorrectionSettings(
        float authoritativeCadenceSeconds,
        float moveSpeed,
        float turnSpeedDegreesPerSecond,
        float snapDistanceMultiplier = 3f,
        float snapAngleMultiplier = 15f)
    {
        AuthoritativeCadenceSeconds = Mathf.Max(0f, authoritativeCadenceSeconds);
        MoveSpeed = Mathf.Max(0f, moveSpeed);
        TurnSpeedDegreesPerSecond = Mathf.Max(0f, turnSpeedDegreesPerSecond);
        SnapDistanceMultiplier = Mathf.Max(1f, snapDistanceMultiplier);
        SnapAngleMultiplier = Mathf.Max(1f, snapAngleMultiplier);
    }

    public float AuthoritativeCadenceSeconds { get; }

    public float MoveSpeed { get; }

    public float TurnSpeedDegreesPerSecond { get; }

    public float SnapDistanceMultiplier { get; }

    public float SnapAngleMultiplier { get; }

    public float MaxBoundedPositionCorrection => MoveSpeed * AuthoritativeCadenceSeconds;

    public float MaxBoundedRotationCorrectionDegrees => TurnSpeedDegreesPerSecond * AuthoritativeCadenceSeconds;

    public float SnapPositionThreshold => MaxBoundedPositionCorrection * SnapDistanceMultiplier;

    public float SnapRotationThresholdDegrees => MaxBoundedRotationCorrectionDegrees * SnapAngleMultiplier;

    public int MaxCorrectionSteps => Mathf.Max(1, Mathf.CeilToInt(SnapDistanceMultiplier));
}

public readonly struct ControlledPlayerVisualCorrectionState
{
    public static ControlledPlayerVisualCorrectionState None => default;

    public ControlledPlayerVisualCorrectionState(Vector3 targetPosition, Quaternion targetRotation, int remainingStepBudget)
    {
        TargetPosition = targetPosition;
        TargetRotation = targetRotation;
        RemainingStepBudget = Mathf.Max(0, remainingStepBudget);
    }

    public Vector3 TargetPosition { get; }

    public Quaternion TargetRotation { get; }

    public int RemainingStepBudget { get; }

    public bool IsActive => RemainingStepBudget > 0;
}

public readonly struct ControlledPlayerCorrectionResult
{
    public ControlledPlayerCorrectionResult(
        Vector3 position,
        Quaternion rotation,
        bool usedHardSnap,
        ControlledPlayerVisualCorrectionState nextState,
        float positionError,
        float rotationErrorDegrees)
    {
        Position = position;
        Rotation = rotation;
        UsedHardSnap = usedHardSnap;
        NextState = nextState;
        PositionError = positionError;
        RotationErrorDegrees = rotationErrorDegrees;
    }

    public Vector3 Position { get; }

    public Quaternion Rotation { get; }

    public bool UsedHardSnap { get; }

    public ControlledPlayerVisualCorrectionState NextState { get; }

    public float PositionError { get; }

    public float RotationErrorDegrees { get; }
}

public static class ControlledPlayerCorrection
{
    public static ControlledPlayerCorrectionResult Resolve(
        Vector3 currentPosition,
        Quaternion currentRotation,
        Vector3 targetPosition,
        Quaternion targetRotation,
        ControlledPlayerCorrectionSettings settings)
    {
        return Resolve(
            currentPosition,
            currentRotation,
            targetPosition,
            targetRotation,
            settings,
            ControlledPlayerVisualCorrectionState.None);
    }

    public static ControlledPlayerCorrectionResult Resolve(
        Vector3 currentPosition,
        Quaternion currentRotation,
        Vector3 targetPosition,
        Quaternion targetRotation,
        ControlledPlayerCorrectionSettings settings,
        ControlledPlayerVisualCorrectionState activeCorrection)
    {
        var positionError = Vector3.Distance(currentPosition, targetPosition);
        var rotationError = Quaternion.Angle(currentRotation, targetRotation);
        var boundedPositionCorrection = settings.MaxBoundedPositionCorrection;
        var boundedRotationCorrection = settings.MaxBoundedRotationCorrectionDegrees;

        if (positionError <= Mathf.Epsilon && rotationError <= Mathf.Epsilon)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, false, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        if (boundedPositionCorrection <= Mathf.Epsilon && boundedRotationCorrection <= Mathf.Epsilon)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        if (positionError > settings.SnapPositionThreshold || rotationError > settings.SnapRotationThresholdDegrees)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        var remainingStepBudget = activeCorrection.IsActive
            ? activeCorrection.RemainingStepBudget
            : settings.MaxCorrectionSteps;
        if (remainingStepBudget <= 0)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        var correctedPosition = Vector3.MoveTowards(currentPosition, targetPosition, boundedPositionCorrection);
        var correctedRotation = Quaternion.RotateTowards(currentRotation, targetRotation, boundedRotationCorrection);
        var nextPositionError = Vector3.Distance(correctedPosition, targetPosition);
        var nextRotationError = Quaternion.Angle(correctedRotation, targetRotation);
        if (nextPositionError <= Mathf.Epsilon && nextRotationError <= Mathf.Epsilon)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, false, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        remainingStepBudget--;
        if (remainingStepBudget <= 0)
        {
            return new ControlledPlayerCorrectionResult(targetPosition, targetRotation, true, ControlledPlayerVisualCorrectionState.None, positionError, rotationError);
        }

        return new ControlledPlayerCorrectionResult(
            correctedPosition,
            correctedRotation,
            false,
            new ControlledPlayerVisualCorrectionState(targetPosition, targetRotation, remainingStepBudget),
            positionError,
            rotationError);
    }
}
