using Godot;
using SupaLidlGame.Extensions;
using System;

namespace SupaLidlGame.State.Thinker;

public partial class AttackState : ThinkerState
{
    [Export]
    public float PreferredWeightDistance
    { 
        get => _preferredWeightDistance;
        protected set
        {
            _preferredWeightDistance = value;
            _preferredWeightDistanceSq = value * value;
        }
    }

    [Export]
    public float MaxWeightDistance
    { 
        get => _maxWeightDistance;
        protected set
        {
            _maxWeightDistance = value;
            _maxWeightDistanceSq = value * value;
        }
    }

    [Export]
    public float MaxDistanceToTarget { get; set; }

    [Export]
    public float UseItemDistance { get; set; } = 40;

    [Export]
    public ThinkerState PassiveState { get; set; }

    [Export]
    public ThinkerState PursueState { get; set; }

    [Export]
    public bool PursueOnNoLOS { get; set; } = true;

    protected Characters.Character _bestTarget;

    protected float _preferredWeightDistance = 64.0f;
    protected float _maxWeightDistance = 8.0f;
    protected float _preferredWeightDistanceSq = 4096.0f;
    protected float _maxWeightDistanceSq = 64.0f;

    public float[] Weights => _weights;

    protected float[] _weights = new float[16];
    protected int _bestWeightIdx;
    protected static readonly Vector2[] _weightDirs = new Vector2[16];

    static AttackState()
    {
        for (int i = 0; i < 16; i++)
        {
            float y = Mathf.Sin(Mathf.Pi * i * 2 / 16);
            float x = Mathf.Cos(Mathf.Pi * i * 2 / 16);
            _weightDirs[i] = new Vector2(x, y);
        }
    }

    public override IState<ThinkerState> Enter(IState<ThinkerState> prev)
    {
        GD.Print("attacking");
        return base.Enter(prev);
    }

    public void UpdateWeights(Vector2 pos)
    {
        // FIXME: TODO: remove all the spaghetti
        Vector2 dir = NPC.Target.Normalized();
        float distSq = NPC.GlobalPosition.DistanceSquaredTo(pos);

        var spaceState = NPC.GetWorld2D().DirectSpaceState;
        var exclude = new Godot.Collections.Array<Godot.Rid>();
        exclude.Add(NPC.GetRid());

        // calculate weights based on distance
        for (int i = 0; i < 16; i++)
        {
            float directDot = _weightDirs[i].Dot(dir);
            // clamp dot from [-1, 1] to [0, 1]
            directDot = (directDot + 1) / 2;

            float strafeDot = Math.Abs(_weightDirs[i].Dot(dir.Clockwise90()));
            float currDirDot = (_weightDirs[i].Dot(NPC.Direction) + 1) / 16;
            strafeDot = Mathf.Pow((strafeDot + 1) / 2, 2) + currDirDot;

            // favor strafing when getting closer
            if (distSq > _preferredWeightDistanceSq)
            {
                _weights[i] = directDot;
            }
            else if (distSq > _maxWeightDistanceSq)
            {
                float dDotWeight = Mathf.Sqrt(distSq / 4096);
                float sDotWeight = 1 - dDotWeight;
                _weights[i] = (dDotWeight * directDot) +
                    (sDotWeight * strafeDot);
            }
            else
            {
                _weights[i] = strafeDot;
            }
        }

        // subtract weights that collide
        for (int i = 0; i < 16; i++)
        {
            var rayParams = new PhysicsRayQueryParameters2D
            {
                Exclude = exclude,
                CollideWithBodies = true,
                From = NPC.GlobalPosition,
                To = NPC.GlobalPosition + (_weightDirs[i] * 24),
                CollisionMask = 1 + 2 + 16
            };

            var result = spaceState.IntersectRay(rayParams);

            // if we hit something
            if (result.Count > 0)
            {
                // then we subtract the value of this from the other weights
                float oldWeight = _weights[i];
                for (int j = 0; j < 16; j++)
                {
                    if (i == j)
                    {
                        _weights[i] = 0;
                    }
                    else
                    {
                        float dot = _weightDirs[i].Dot(_weightDirs[j]);
                        if (dot >= 0)
                        {
                            _weights[j] -= _weights[j] * dot;
                        }
                    }
                }
            }
        }


        float bestWeight = 0;
        for (int i = 0; i < 16; i++)
        {
            if (_weights[i] > bestWeight)
            {
                _bestWeightIdx = i;
                bestWeight = _weights[i];
            }
        }
    }

    public override ThinkerState Think()
    {
        // TODO: the entity should wander if it doesn't find a best target
        var bestTarget = NPC.FindBestTarget();
        _bestTarget = bestTarget;

        if (bestTarget is not null)
        {
            Vector2 pos = NPC.LastSeenPosition = bestTarget.GlobalPosition;
            NPC.Target = pos - NPC.GlobalPosition;
            Vector2 dir = NPC.Target;
            float dist = NPC.GlobalPosition.DistanceTo(pos);

            if (PursueState is not null)
            {
                // pursue the player if they can not be seen or is too far away
                if (dist > MaxDistanceToTarget)
                {
                    return PursueState;
                }

                if (PursueOnNoLOS && !NPC.HasLineOfSight(bestTarget))
                {
                    return PursueState;
                }
            }

            NPC.LastSeenPosition = bestTarget.GlobalPosition;

            UpdateWeights(pos);

            if (dist <= UseItemDistance && NPC.CanAttack)
            {
                Attack(bestTarget);
            }
        }
        else
        {
            return PassiveState;
        }

        return base.Think();
    }

    public override ThinkerState Process(double delta)
    {
        if (!NPC.ShouldMove ||
            (!NPC.ShouldMoveWhenUsingItem && NPC.Inventory.IsUsingItem))
        {
            NPC.Direction = Vector2.Zero;
        }
        else
        {
            NPC.Direction = _weightDirs[_bestWeightIdx];
        }

        return base.Process(delta);
    }

    public virtual void Attack(Characters.Character bestTarget)
    {
        if (NPC.Inventory.SelectedItem is Items.Weapon weapon)
        {
            NPC.UseCurrentItem();
        }
    }
}
