using System.Numerics;
using Voile.Resources;
using Voile.Resources.DataReaders;
using Voile.VFS;

namespace Voile.Systems.Particles;

public struct Particle
{
    public Particle()
    {
    }

    public int EmitterIndex { get; set; }
    public Color Color { get; set; }
    public Vector2 Position { get; set; }
    public float LifeTime { get; set; } = 1.0f;
    public float Scale { get; set; }
    public float Rotation { get; set; }
}

public class ParticleEmitterSettings
{
    public bool Local { get; set; } = true;
    public int MaxParticles { get; set; } = 16;
    public float EmitRadius { get; set; }
    public float LifeTime { get; set; } = 1.0f;
    public float Explosiveness { get; set; }
    public Vector2 Direction { get; set; } = -Vector2.UnitY;
    public float LinearVelocity { get; set; } = 980.0f;
    public float AngularVelocity { get; set; }
    public float AngularVelocityRandom { get; set; } = 0.5f;
    public float LinearVelocityRandom { get; set; } = 0.5f;
    public Vector2 Gravity { get; set; } = Vector2.UnitY * 980f;
    public float ScaleBegin { get; set; } = 1.0f;
    public float ScaleEnd { get; set; } = 0.0f;
    public Color ColorBegin { get; set; } = Color.White;
    public Color ColorEnd { get; set; } = Color.Black;
    public float LinearVelocityDamping { get; set; } = 1.0f;
    public float AngularVelocityDamping { get; set; } = 0.1f;
}

public class ParticleEmitterSettingsResource : Resource
{
    public ParticleEmitterSettings Settings { get; private set; }
    public ParticleEmitterSettingsResource(string path, ParticleEmitterSettings settings) : base(path)
    {
        Settings = settings;
    }
}

/// <summary>
/// Loads <see cref="ParticleEmitterSettingsResource"/> from a provided TOML data file.
/// </summary>
public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmitterSettingsResource>
{
    public override IEnumerable<string> SupportedExtensions => new string[] {
        ".toml"
    };

    protected override ParticleEmitterSettingsResource LoadResource(string path)
    {
        var settings = new ParticleEmitterSettings();

        var reader = new TomlDataReader();
        reader.Read(VirtualFileSystem.Read(path));

        settings.Local = reader.GetBool("Local", true);
        settings.MaxParticles = reader.GetInt("MaxParticles");
        settings.EmitRadius = reader.GetInt("EmitRadius");
        settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
        settings.Explosiveness = reader.GetFloat("Explosiveness");
        settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
        settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
        settings.AngularVelocity = reader.GetFloat("AngularVelocity");
        settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
        settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
        settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
        settings.ScaleBegin = reader.GetFloat("ScaleBegin");
        settings.ScaleEnd = reader.GetFloat("ScaleEnd");
        settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
        settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
        settings.LinearVelocityDamping = reader.GetFloat("LinearVelocityDamping", 1.0f);
        settings.AngularVelocityDamping = reader.GetFloat("AngularVelocityDamping", 1.0f);

        return new ParticleEmitterSettingsResource(path, settings);
    }
}

/// <summary>
/// Emits and simulates particles from a provided particle segment.
/// </summary>
public class ParticleEmitter : IUpdatableSystem
{
    /// <summary>
    /// Origin position in the world of this emitter.
    /// </summary>
    public Vector2 OriginPosition { get; set; }
    /// <summary>
    /// <see cref="ParticleEmitterSettings"/> for this emitter.
    /// </summary>
    public ParticleEmitterSettings Settings => _settingsResource.Value.Settings;

    /// <summary>
    /// Constructs a new <see cref="ParticleEmitter"/>.
    /// </summary>
    /// <param name="originPosition">World origin position.</param>
    /// <param name="settingsResource">Emitter settings resource.</param>
    /// <param name="particles">Particle segment that this emitter will simulate.</param>
    public ParticleEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource)
    {
        OriginPosition = originPosition;

        _settingsResource = settingsResource;
        _maxParticles = _settingsResource.Value.Settings.MaxParticles;

        _positions = new Vector2[_maxParticles];
        _velocities = new Vector2[_maxParticles];
        _angularVelocities = new float[_maxParticles];
        _lifetimes = new float[_maxParticles];
        _rotations = new float[_maxParticles];

        _random = new LehmerRandom();
    }

    public Particle GetParticle(int idx)
    {
        if (idx > _lifetimes.Length)
        {
            return new Particle();
        }

        var t = _lifetimes[idx] / Settings.LifeTime;
        var scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t);
        var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t);

        return new Particle()
        {
            Position = _positions[idx],
            Scale = scale,
            Color = color,
            Rotation = _rotations[idx]
        };
    }

    /// <summary>
    /// Restart this emitter.
    /// </summary>
    /// <param name="particles">New particle segment.</param>
    public void Restart()
    {
        _maxParticles = Settings.MaxParticles;

        _positions = new Vector2[_maxParticles];
        _velocities = new Vector2[_maxParticles];
        _angularVelocities = new float[_maxParticles];
        _lifetimes = new float[_maxParticles];
        _rotations = new float[_maxParticles];

        _particleIndex = 0;
    }

    /// <summary>
    /// Updates this emitter's simulation.
    /// </summary>
    /// <param name="deltaTime"></param>
    public void Update(double deltaTime)
    {
        var rate = (int)MathUtils.Lerp(1, _maxParticles, Settings.Explosiveness);

        var deltaTimeVector = new Vector2((float)deltaTime);
        var gravityVector = Settings.Gravity * (float)deltaTime;

        var linearVelocityDampingFactor = Settings.LinearVelocityDamping * (float)deltaTime;
        var angularVelocityDampingFactor = Settings.AngularVelocityDamping * (float)deltaTime;

        for (int i = 0; i < _maxParticles; i++)
        {
            _lifetimes[i] = Math.Max(0.0f, _lifetimes[i] - (float)deltaTime);

            _velocities[i] += gravityVector;

            _positions[i] += _velocities[i] * deltaTimeVector;
            _rotations[i] += _angularVelocities[i] * (float)deltaTime;

            _velocities[i] -= _velocities[i] * linearVelocityDampingFactor;
            _angularVelocities[i] -= _angularVelocities[i] * angularVelocityDampingFactor;
        }

        for (int i = 0; i < rate; i++)
        {
            Emit();
        }
    }

    private void Emit()
    {
        if (_lifetimes[_particleIndex] > 0) return;

        _positions[_particleIndex] = GetEmitPosition();
        _velocities[_particleIndex] = Settings.Direction * Settings.LinearVelocity;

        _velocities[_particleIndex] += Vector2.One * Settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
        _angularVelocities[_particleIndex] = Settings.AngularVelocity * Settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);

        _lifetimes[_particleIndex] = Settings.LifeTime;

        _particleIndex = (_particleIndex + 1) % _maxParticles;
    }

    private Vector2 GetEmitPosition()
    {
        var settings = _settingsResource.Value.Settings;
        Vector2 originOffset = Settings.Local ? OriginPosition : Vector2.Zero;

        // https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
        var angle = _random.NextDouble() * Math.PI * 2;
        float radius = (float)Math.Sqrt(_random.NextDouble()) * settings.EmitRadius;

        float x = radius * (float)Math.Cos(angle);
        float y = radius * (float)Math.Sin(angle);

        var point = new Vector2(x, y);

        return point + originOffset;
    }

    private LehmerRandom _random;
    private int _maxParticles;
    private int _particleIndex;
    private Vector2[] _positions, _velocities;
    private float[] _lifetimes, _rotations, _angularVelocities;
    private ResourceRef<ParticleEmitterSettingsResource> _settingsResource;
}

/// <summary>
/// CPU based particle simulator.
/// </summary>
public class ParticleSystem : IUpdatableSystem, IDisposable
{
    /// <summary>
    /// List of particle emitters created for this ParticleSystem.
    /// </summary>
    public IReadOnlyList<ParticleEmitter> Emitters => _emitters;

    /// <summary>
    /// Creates an emitter from a <see cref="ParticleEmitterSettingsResource"/>.
    /// </summary>
    /// <param name="originPosition"></param>
    /// <param name="settingsResource"></param>
    /// <returns></returns>
    public int CreateEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource)
    {
        var emitter = new ParticleEmitter(originPosition, settingsResource);
        _emitters.Add(emitter);
        return _emitters.Count - 1;
    }

    public void RemoveEmitter(int emitterIdx)
    {
        if (emitterIdx >= _emitters.Count)
        {
            throw new ArgumentException($"Emitter with idx {emitterIdx} is not present!");
        }

        // var emitter = _emitters[emitterIdx];
        _emitters.RemoveAt(emitterIdx);
    }

    public void SetEmitterPosition(int emitterIdx, Vector2 position)
    {
        if (emitterIdx >= _emitters.Count)
        {
            throw new ArgumentException($"Emitter with idx {emitterIdx} is not present!");
        }

        var emitter = _emitters[emitterIdx];
        emitter.OriginPosition = position;
    }

    /// <summary>
    /// Restarts an emitter. 
    /// </summary>
    /// <param name="id">Id of an emitter to restart.</param>
    /// <exception cref="ArgumentException"></exception>
    public void RestartEmitter(int id)
    {
        if (id > _emitters.Count - 1)
        {
            throw new ArgumentException($"Emitter with id {id} doesn't exist!");
        }

        var emitter = _emitters[id];
        emitter.Restart();
    }

    /// <summary>
    /// Updates this particle simulation. It is recommended to use a fixed timestep for the particle simulation for performance reasons.
    /// </summary>
    /// <param name="deltaTime"></param>
    public void Update(double deltaTime)
    {
        foreach (var emitter in _emitters)
        {
            emitter.Update(deltaTime);
        }
    }

    public void Dispose()
    {
    }

    private List<ParticleEmitter> _emitters = new();
}