using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.UI;
using Microsoft.Graphics.Canvas.UI.Xaml;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Windows.Foundation;

using Windows.UI;

namespace Sonex.Client.Controls;

public sealed partial class AnimatedBackground : UserControl, IDisposable
{
    private readonly Dictionary<long, List<int>> _grid = [];
    private readonly Dictionary<int, int> _idToIndex = [];

    private readonly List<Node> _nodes = [];
    private readonly List<Edge> _edges = [];
    private readonly List<Particle> _particles = [];
    private readonly List<Rocket> _rockets = [];
    private readonly List<CanvasBitmap> _iconBitmaps = [];

    private readonly Stack<Particle> _particlePool = new();
    private readonly Stack<Rocket> _rocketPool = new();

    private CanvasBitmap? _backgroundBitmap;
    private CanvasRenderTarget? _sceneLayer;
    private CanvasRenderTarget? _iconLayer;
    private CanvasRenderTarget? _fireworksLayer;

    private readonly ColorSourceEffect _fireworksFadeSource = new();

    private AnimatedBackgroundAnimationSettings _settings = new();
    private AnimatedBackgroundAnimationSettings _pendingSettings = new();

    private bool _isRunning;
    private bool _seeded;
    private bool _settingsDirty = true;
    private bool _assetsDirty = true;
    private bool _assetsLoading;

    private string? _lastSettingsJson;

    private double _time;
    private double _dtLast = 1.0 / 60.0;
    private double _spawnAcc;
    private double _fireworksAccumulator;
    private const double FireworksFixedStep = 1.0 / 30.0;

    private double _lastFrameTimeSeconds;
    private double _lastFireworksLaunchMs;
    private int _nextId = 1;
    private int _frameCounter;

    private float _layerWidthDip = 1;
    private float _layerHeightDip = 1;
    private float _layerDpi = 96;

    private readonly StopwatchEx _clock = new();

    private const int MaxParticlePoolSize = 2000;
    private const int MaxRocketPoolSize = 32;

    public AnimatedBackground()
    {
        InitializeComponent();

        _settings = NormalizeSettings(Animation ?? new AnimatedBackgroundAnimationSettings());
        _pendingSettings = CloneSettings(_settings);
        _lastSettingsJson = SerializeSettings(_settings);

        Loaded += AnimatedBackground_Loaded;
        Unloaded += AnimatedBackground_Unloaded;
    }

    public static readonly DependencyProperty ChildContentProperty =
        DependencyProperty.Register(
            nameof(ChildContent),
            typeof(UIElement),
            typeof(AnimatedBackground),
            new PropertyMetadata(null));

    public UIElement? ChildContent
    {
        get => (UIElement?)GetValue(ChildContentProperty);
        set => SetValue(ChildContentProperty, value);
    }

    public static readonly DependencyProperty AnimationProperty =
        DependencyProperty.Register(
            nameof(Animation),
            typeof(AnimatedBackgroundAnimationSettings),
            typeof(AnimatedBackground),
            new PropertyMetadata(new AnimatedBackgroundAnimationSettings(), OnAnimationChanged));

    public AnimatedBackgroundAnimationSettings Animation
    {
        get => (AnimatedBackgroundAnimationSettings)GetValue(AnimationProperty);
        set => SetValue(AnimationProperty, value);
    }

    public static readonly DependencyProperty BackgroundBrushExProperty =
        DependencyProperty.Register(
            nameof(BackgroundBrushEx),
            typeof(Brush),
            typeof(AnimatedBackground),
            new PropertyMetadata(new SolidColorBrush(Colors.Transparent)));

    public Brush BackgroundBrushEx
    {
        get => (Brush)GetValue(BackgroundBrushExProperty);
        set => SetValue(BackgroundBrushExProperty, value);
    }

    public static readonly DependencyProperty BorderBrushExProperty =
        DependencyProperty.Register(
            nameof(BorderBrushEx),
            typeof(Brush),
            typeof(AnimatedBackground),
            new PropertyMetadata(new SolidColorBrush(Color.FromArgb(255, 216, 216, 216))));

    public Brush BorderBrushEx
    {
        get => (Brush)GetValue(BorderBrushExProperty);
        set => SetValue(BorderBrushExProperty, value);
    }

    public static readonly DependencyProperty BorderThicknessExProperty =
        DependencyProperty.Register(
            nameof(BorderThicknessEx),
            typeof(Thickness),
            typeof(AnimatedBackground),
            new PropertyMetadata(new Thickness(0)));

    public Thickness BorderThicknessEx
    {
        get => (Thickness)GetValue(BorderThicknessExProperty);
        set => SetValue(BorderThicknessExProperty, value);
    }

    public static readonly DependencyProperty CornerRadiusExProperty =
        DependencyProperty.Register(
            nameof(CornerRadiusEx),
            typeof(CornerRadius),
            typeof(AnimatedBackground),
            new PropertyMetadata(new CornerRadius(0)));

    public CornerRadius CornerRadiusEx
    {
        get => (CornerRadius)GetValue(CornerRadiusExProperty);
        set => SetValue(CornerRadiusExProperty, value);
    }

    private static void OnAnimationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var control = (AnimatedBackground)d;
        var nextRaw = e.NewValue as AnimatedBackgroundAnimationSettings ?? new AnimatedBackgroundAnimationSettings();
        var next = NormalizeSettings(nextRaw);
        var json = SerializeSettings(next);

        if (json == control._lastSettingsJson)
            return;

        control._assetsDirty = AreAssetSourcesDifferent(control._settings, next);
        control._lastSettingsJson = json;
        control._pendingSettings = CloneSettings(next);
        control._settingsDirty = true;
        control.AnimatedCanvas?.Invalidate();
    }

    private void AnimatedBackground_Loaded(object sender, RoutedEventArgs e)
    {
        if (_isRunning)
            return;

        _clock.Restart();
        _lastFrameTimeSeconds = _clock.ElapsedTotalSeconds;
        CompositionTarget.Rendering += CompositionTarget_Rendering;
        _isRunning = true;
    }

    private void AnimatedBackground_Unloaded(object sender, RoutedEventArgs e)
    {
        if (!_isRunning)
            return;

        CompositionTarget.Rendering -= CompositionTarget_Rendering;
        _isRunning = false;
        DisposeResources();
    }

    private void AnimatedCanvas_CreateResources(CanvasControl sender, CanvasCreateResourcesEventArgs args)
    {
        args.TrackAsyncAction(LoadAssetsAsync(sender).AsAsyncAction());
    }

    private void CompositionTarget_Rendering(object? sender, object e)
    {
        if (!_isRunning)
            return;

        ApplyPendingSettingsIfNeeded();

        var nowSeconds = _clock.ElapsedTotalSeconds;
        var dt = nowSeconds - _lastFrameTimeSeconds;
        _lastFrameTimeSeconds = nowSeconds;

        if (dt <= 0)
            return;

        dt = Math.Min(dt, 0.05);
        _dtLast = dt;
        _time += dt;
        _frameCounter++;

        Update(dt, nowSeconds * 1000.0);
        AnimatedCanvas.Invalidate();
    }

    private async void ApplyPendingSettingsIfNeeded()
    {
        if (!_settingsDirty)
            return;

        _settings = NormalizeSettings(_pendingSettings);
        _settingsDirty = false;

        ResetAnimationState();

        if (!_assetsDirty || _assetsLoading)
        {
            AnimatedCanvas?.Invalidate();
            return;
        }

        _assetsLoading = true;

        try
        {
            await LoadAssetsAsync(AnimatedCanvas);
        }
        finally
        {
            _assetsLoading = false;
            _assetsDirty = false;
            AnimatedCanvas?.Invalidate();
        }
    }

    private void ResetAnimationState()
    {
        _grid.Clear();
        _idToIndex.Clear();

        _nodes.Clear();
        _edges.Clear();

        RecycleAllParticles();
        RecycleAllRockets();

        _seeded = false;
        _spawnAcc = 0;
        _fireworksAccumulator = 0;
        _lastFireworksLaunchMs = 0;
        _nextId = 1;
        _frameCounter = 0;

        _time = 0;
        _dtLast = 1.0 / 60.0;

        ClearAllLayers();
    }

    private void ClearAllLayers()
    {
        ClearLayer(_sceneLayer);
        ClearLayer(_iconLayer);
        ClearLayer(_fireworksLayer);
    }

    private static void ClearLayer(CanvasRenderTarget? layer)
    {
        if (layer is null)
            return;

        using var ds = layer.CreateDrawingSession();
        ds.Clear(Colors.Transparent);
    }

    private static AnimatedBackgroundAnimationSettings CloneSettings(AnimatedBackgroundAnimationSettings s)
        => JsonSerializer.Deserialize<AnimatedBackgroundAnimationSettings>(
               JsonSerializer.Serialize(s))
           ?? new AnimatedBackgroundAnimationSettings();

    private static string SerializeSettings(AnimatedBackgroundAnimationSettings s)
        => JsonSerializer.Serialize(s);

    private static bool AreAssetSourcesDifferent(
        AnimatedBackgroundAnimationSettings current,
        AnimatedBackgroundAnimationSettings next)
    {
        if (!string.Equals(current.BackgroundImageUrl, next.BackgroundImageUrl, StringComparison.Ordinal))
            return true;

        if (current.IconUris.Length != next.IconUris.Length)
            return true;

        for (var i = 0; i < current.IconUris.Length; i++)
        {
            if (!string.Equals(current.IconUris[i], next.IconUris[i], StringComparison.Ordinal))
                return true;
        }

        return false;
    }

    private async Task LoadAssetsAsync(ICanvasResourceCreator resourceCreator)
    {
        DisposeBitmaps();

        _backgroundBitmap = await LoadBitmapOrNullAsync(resourceCreator, _settings.BackgroundImageUrl);

        foreach (var uri in _settings.IconUris)
        {
            var bitmap = await LoadBitmapOrNullAsync(resourceCreator, uri);
            if (bitmap is not null)
                _iconBitmaps.Add(bitmap);
        }

        foreach (var node in _nodes)
            AssignNodeIcon(node);
    }

    private static async Task<CanvasBitmap?> LoadBitmapOrNullAsync(ICanvasResourceCreator resourceCreator, string? pathOrUri)
    {
        if (string.IsNullOrWhiteSpace(pathOrUri))
            return null;

        try
        {
            if (Uri.TryCreate(pathOrUri, UriKind.Absolute, out var absoluteUri))
                return await CanvasBitmap.LoadAsync(resourceCreator, absoluteUri);

            if (File.Exists(pathOrUri))
                return await CanvasBitmap.LoadAsync(resourceCreator, pathOrUri);

            var fullPath = Path.Combine(AppContext.BaseDirectory, pathOrUri);
            if (File.Exists(fullPath))
                return await CanvasBitmap.LoadAsync(resourceCreator, fullPath);

            var normalized = pathOrUri.Replace("\\", "/").TrimStart('/');
            return await CanvasBitmap.LoadAsync(resourceCreator, new Uri($"ms-appx:///{normalized}"));
        }
        catch
        {
            return null;
        }
    }

    private void AnimatedCanvas_Draw(CanvasControl sender, CanvasDrawEventArgs args)
    {
        if (sender.ActualWidth <= 0 || sender.ActualHeight <= 0)
            return;

        EnsureRenderTargets(sender);

        UpdateFireworksLayer();
        RenderSceneBase();
        RenderIconLayerIfNeeded();

        var ds = args.DrawingSession;
        ds.Clear(Colors.Transparent);

        if (UsesEffectBlend(_settings.BlendMode))
        {
            if (_sceneLayer is null || _iconLayer is null)
                return;

            using var effect = new BlendEffect
            {
                Mode = ToBlendEffectMode(_settings.BlendMode),
                Background = _sceneLayer,
                Foreground = _iconLayer
            };

            ds.DrawImage(effect);
            return;
        }

        if (_sceneLayer is not null)
            ds.DrawImage(_sceneLayer);

        DrawIconsDirect(ds, _settings.BlendMode);
    }

    private void EnsureRenderTargets(CanvasControl sender)
    {
        var width = Math.Max(1f, (float)sender.ActualWidth);
        var height = Math.Max(1f, (float)sender.ActualHeight);
        var dpi = sender.Dpi;

        if (_sceneLayer is not null &&
            _iconLayer is not null &&
            _fireworksLayer is not null &&
            Math.Abs(_layerWidthDip - width) < 0.1f &&
            Math.Abs(_layerHeightDip - height) < 0.1f &&
            Math.Abs(_layerDpi - dpi) < 0.1f)
        {
            return;
        }

        _sceneLayer?.Dispose();
        _iconLayer?.Dispose();
        _fireworksLayer?.Dispose();

        _layerWidthDip = width;
        _layerHeightDip = height;
        _layerDpi = dpi;

        _sceneLayer = new CanvasRenderTarget(sender, width, height, dpi);
        _iconLayer = new CanvasRenderTarget(sender, width, height, dpi);
        _fireworksLayer = new CanvasRenderTarget(sender, width, height, dpi);

        using var ds = _fireworksLayer.CreateDrawingSession();
        ds.Clear(Colors.Transparent);
    }

    private void Update(double dt, double nowMs)
    {
        if (!_seeded)
        {
            var initial = Math.Min(_settings.MaxNodes, 40);
            Seed(initial);
            _seeded = true;
        }

        UpdateFireworksFixed(dt, nowMs);
        ApplyEdgeSprings(dt);
        UpdateMotion(dt);
        UpdateSpawning(dt);

        if (_settings.CollisionIterations > 0 &&
            _nodes.Count > 1 &&
            (_nodes.Count < 14 || (_frameCounter & 1) == 0))
        {
            ResolveOverlaps();
        }

        CullExitedNodes();
    }

    private void RenderSceneBase()
    {
        if (_sceneLayer is null)
            return;

        using var ds = _sceneLayer.CreateDrawingSession();
        ds.Clear(Colors.Transparent);

        DrawBackground(ds);
        DrawFireworksLayer(ds);

        if (_settings.DrawEdges)
            DrawEdges(ds);

        DrawNodeFills(ds);
    }

    private void RenderIconLayerIfNeeded()
    {
        if (_iconLayer is null)
            return;

        using var ds = _iconLayer.CreateDrawingSession();
        ds.Clear(Colors.Transparent);

        if (!UsesEffectBlend(_settings.BlendMode))
            return;

        DrawIconsNormal(ds);
    }

    private void DrawBackground(CanvasDrawingSession ds)
    {
        if (_backgroundBitmap is null)
        {
            if (TryParseCssColor(_settings.BackgroundColor, out var color) && color.A > 0)
                ds.Clear(color);

            return;
        }

        DrawBitmapByCssFit(
            ds,
            _backgroundBitmap,
            new Rect(0, 0, _layerWidthDip, _layerHeightDip),
            _settings.BackgroundSize,
            _settings.BackgroundPosition,
            _settings.BackgroundRepeat);
    }

    private static void DrawBitmapByCssFit(
        CanvasDrawingSession ds,
        CanvasBitmap bitmap,
        Rect bounds,
        string? size,
        string? position,
        string? repeat)
    {
        var repeatText = (repeat ?? "no-repeat").Trim().ToLowerInvariant();

        if (repeatText != "no-repeat")
        {
            DrawRepeatedBitmap(ds, bitmap, bounds, repeatText);
            return;
        }

        var iw = bitmap.Size.Width;
        var ih = bitmap.Size.Height;
        var sizeText = (size ?? "cover").Trim().ToLowerInvariant();
        var posText = (position ?? "center center").Trim().ToLowerInvariant();

        var dw = bounds.Width;
        var dh = bounds.Height;

        if (sizeText == "contain")
        {
            var scale = Math.Min(bounds.Width / iw, bounds.Height / ih);
            dw = iw * scale;
            dh = ih * scale;
        }
        else if (sizeText == "cover")
        {
            var scale = Math.Max(bounds.Width / iw, bounds.Height / ih);
            dw = iw * scale;
            dh = ih * scale;
        }
        else
        {
            dw = iw;
            dh = ih;
        }

        var dx = bounds.X + (bounds.Width - dw) * 0.5;
        var dy = bounds.Y + (bounds.Height - dh) * 0.5;

        if (posText.Contains("left")) dx = bounds.X;
        if (posText.Contains("right")) dx = bounds.X + bounds.Width - dw;
        if (posText.Contains("top")) dy = bounds.Y;
        if (posText.Contains("bottom")) dy = bounds.Y + bounds.Height - dh;

        ds.DrawImage(bitmap, new Rect(dx, dy, dw, dh));
    }

    private static void DrawRepeatedBitmap(CanvasDrawingSession ds, CanvasBitmap bitmap, Rect bounds, string repeat)
    {
        var tileW = bitmap.Size.Width;
        var tileH = bitmap.Size.Height;

        if (tileW <= 0 || tileH <= 0)
            return;

        var repeatX = repeat is "repeat" or "repeat-x";
        var repeatY = repeat is "repeat" or "repeat-y";

        var startX = repeatX ? bounds.X - tileW : bounds.X;
        var endX = repeatX ? bounds.Right + tileW : bounds.X + tileW;

        var startY = repeatY ? bounds.Y - tileH : bounds.Y;
        var endY = repeatY ? bounds.Bottom + tileH : bounds.Y + tileH;

        for (double y = startY; y < endY; y += tileH)
        {
            for (double x = startX; x < endX; x += tileW)
            {
                ds.DrawImage(bitmap, new Rect(x, y, tileW, tileH));
            }
        }
    }

    private void UpdateFireworksLayer()
    {
        if (_fireworksLayer is null)
            return;

        using var ds = _fireworksLayer.CreateDrawingSession();

        if (!_settings.FireworksEnabled)
        {
            ds.Clear(Colors.Transparent);
            return;
        }

        var alpha = (byte)Math.Round(Clamp(_settings.FireworksFadeFillAlpha, 0, 1) * 255);
        _fireworksFadeSource.Color = Color.FromArgb(alpha, 0, 0, 0);

        ds.DrawImage(
            _fireworksFadeSource,
            new Rect(0, 0, _layerWidthDip, _layerHeightDip),
            new Rect(0, 0, _layerWidthDip, _layerHeightDip),
            1f,
            CanvasImageInterpolation.NearestNeighbor,
            CanvasComposite.DestinationOut);

        for (var i = 0; i < _rockets.Count; i++)
            _rockets[i].Render(ds, _settings.FireworksOpacity);

        for (var i = 0; i < _particles.Count; i++)
            _particles[i].Render(ds, _settings.FireworksOpacity);
    }

    private void DrawFireworksLayer(CanvasDrawingSession ds)
    {
        if (!_settings.FireworksEnabled || _fireworksLayer is null)
            return;

        ds.DrawImage(_fireworksLayer);
    }

    private void DrawEdges(CanvasDrawingSession ds)
    {
        for (var i = 0; i < _edges.Count; i++)
        {
            var e = _edges[i];
            var a = GetNodeById(e.A);
            var b = GetNodeById(e.B);

            if (a is null || b is null)
                continue;

            var alpha = Math.Min(a.Alpha, b.Alpha) * 0.55;
            if (alpha <= 0.001)
                continue;

            ds.DrawLine(
                (float)a.Pos.X,
                (float)a.Pos.Y,
                (float)b.Pos.X,
                (float)b.Pos.Y,
                Color.FromArgb((byte)Math.Round(alpha * 255), 160, 160, 160),
                0.3f);
        }
    }

    private void DrawNodeFills(CanvasDrawingSession ds)
    {
        if (!_settings.FillNodes)
            return;

        for (var i = 0; i < _nodes.Count; i++)
        {
            var n = _nodes[i];
            var fillAlpha = Clamp(n.Alpha * 0.95 * _settings.NodesOpacity, 0, 1);
            ds.FillCircle(
                (float)n.Pos.X,
                (float)n.Pos.Y,
                (float)n.Radius,
                WithAlpha(n.FillColor, fillAlpha));
        }
    }

    private void DrawIconsNormal(CanvasDrawingSession ds)
    {
        for (var i = 0; i < _nodes.Count; i++)
            DrawSingleNodeIcon(ds, _nodes[i]);
    }

    private void DrawIconsDirect(CanvasDrawingSession ds, AnimatedBackgroundBlendMode blendMode)
    {
        var oldBlend = ds.Blend;
        ds.Blend = ToCanvasBlend(blendMode);

        try
        {
            for (var i = 0; i < _nodes.Count; i++)
                DrawSingleNodeIcon(ds, _nodes[i]);
        }
        finally
        {
            ds.Blend = oldBlend;
        }
    }

    private void DrawSingleNodeIcon(CanvasDrawingSession ds, Node n)
    {
        if (!_settings.DrawIcons)
            return;

        if (n.IconIndex < 0 || n.IconIndex >= _iconBitmaps.Count)
            return;

        if ((n.Radius * 2) < _settings.IconMinDiameter)
            return;

        var icon = _iconBitmaps[n.IconIndex];
        var baseD = n.Radius * 2 * _settings.IconScale;
        var widthScale = 1.0;

        if (_settings.PulseIconWidth && _settings.PulseIconWidthAmplitude > 0)
        {
            var s = Math.Sin((_time * _settings.PulseIconWidthFrequency + n.Phase * 0.37) * Math.PI * 2);
            widthScale = Clamp(1 + s * _settings.PulseIconWidthAmplitude, 0.65, 1.35);
        }

        var dw = baseD * widthScale;
        var dh = baseD;
        var x = n.Pos.X - dw * 0.5;
        var y = n.Pos.Y - dh * 0.5;

        var alpha = Clamp(n.Alpha * _settings.NodesOpacity, 0, 1);
        if (alpha <= 0.01)
            return;

        ds.DrawImage(
            icon,
            new Rect(x, y, dw, dh),
            new Rect(0, 0, icon.Size.Width, icon.Size.Height),
            (float)alpha,
            CanvasImageInterpolation.Linear);
    }

    private void UpdateFireworksFixed(double dt, double nowMs)
    {
        if (!_settings.FireworksEnabled)
        {
            RecycleAllRockets();
            RecycleAllParticles();
            _fireworksAccumulator = 0;
            return;
        }

        _fireworksAccumulator += dt;

        while (_fireworksAccumulator >= FireworksFixedStep)
        {
            _fireworksAccumulator -= FireworksFixedStep;
            UpdateFireworksStep(nowMs);
        }
    }

    private void UpdateFireworksStep(double nowMs)
    {
        if (nowMs - _lastFireworksLaunchMs >= _settings.FireworksLaunchIntervalMs)
        {
            _lastFireworksLaunchMs = nowMs;
            LaunchRocket();
        }

        for (var i = _rockets.Count - 1; i >= 0; i--)
        {
            var rocket = _rockets[i];
            rocket.Update();

            var randomChance = rocket.Y < (_layerHeightDip * 2.0 / 3.0)
                ? (Random.Shared.NextDouble() * 100.0 <= 1.0)
                : false;

            if (rocket.Y < _layerHeightDip / 5.0 || rocket.Vy >= 0 || randomChance)
            {
                rocket.Explode(this);
                _rockets.RemoveAt(i);
                RecycleRocket(rocket);
            }
        }

        for (var i = _particles.Count - 1; i >= 0; i--)
        {
            var particle = _particles[i];
            particle.Update();

            if (!particle.Exists())
            {
                _particles.RemoveAt(i);
                RecycleParticle(particle);
            }
        }

        while (_particles.Count > _settings.FireworksMaxParticles)
        {
            var particle = _particles[0];
            _particles.RemoveAt(0);
            RecycleParticle(particle);
        }
    }

    private void LaunchRocket()
    {
        if (_rockets.Count >= _settings.FireworksMaxRockets)
            return;

        var x = _layerWidthDip / 2.0;
        var startY = Clamp(_layerHeightDip - _settings.FireworksLaunchBottomOffset, 0, _layerHeightDip);

        var rocket = RentRocket(x, startY);
        var colors = _settings.FireworksColors;

        if (colors.Length > 0)
        {
            rocket.HasExplosionColor = true;
            rocket.ExplosionColor = ParseHexColor(colors[Random.Shared.Next(colors.Length)], Colors.White);
        }
        else
        {
            rocket.HasExplosionColor = false;
            rocket.ExplosionHue = Random.Shared.Next(36) * 10;
        }

        rocket.Vy = Random.Shared.NextDouble() * -_settings.FireworksRocketVelocityRange - _settings.FireworksRocketVelocityMin;
        rocket.Vx = Random.Shared.NextDouble() * (_settings.FireworksRocketHorizontalVelocity * 2.0) - _settings.FireworksRocketHorizontalVelocity;
        rocket.Size = _settings.FireworksRocketSize;
        rocket.Shrink = 0.999;
        rocket.Gravity = _settings.FireworksRocketGravity;
        rocket.Alpha = 1;
        rocket.Fade = 0;
        rocket.Resistance = 1;

        _rockets.Add(rocket);
    }

    private void Seed(int initialCount)
    {
        _nodes.Clear();
        _edges.Clear();
        _idToIndex.Clear();
        _spawnAcc = 0;
        _nextId = 1;

        var rect = GetMovementBoundsRect();
        var placedNodes = new List<Node>();
        const int maxAttemptsPerNode = 300;
        var padding = _settings.CollisionPadding;

        var radii = new List<double>();

        for (var i = 0; i < initialCount; i++)
        {
            radii.Add(
                _settings.MinNodeRadius +
                Random.Shared.NextDouble() * (_settings.MaxNodeRadius - _settings.MinNodeRadius));
        }

        radii.Sort((a, b) => b.CompareTo(a));

        foreach (var radius in radii)
        {
            Node? placedNode = null;

            for (var attempt = 0; attempt < maxAttemptsPerNode; attempt++)
            {
                var x = rect.Left + radius + Random.Shared.NextDouble() * Math.Max(1, rect.Width - radius * 2);
                var y = rect.Top + radius + Random.Shared.NextDouble() * Math.Max(1, rect.Height - radius * 2);

                if (!IsPositionFree(x, y, radius, placedNodes, padding))
                    continue;

                placedNode = CreateNodeAt(x, y, radius, 1);
                placedNodes.Add(placedNode);
                _nodes.Add(placedNode);
                break;
            }
        }

        Reindex();

        for (var i = 0; i < _nodes.Count; i++)
            ConnectNewNode(_nodes[i].Id);

        CompactEdges();
    }

    private bool IsPositionFree(double x, double y, double radius, List<Node> placedNodes, double extraPadding)
    {
        var rect = GetMovementBoundsRect();

        var minX = rect.Left + radius;
        var maxX = rect.Right - radius;
        var minY = rect.Top + radius;
        var maxY = rect.Bottom - radius;

        if (x < minX || x > maxX || y < minY || y > maxY)
            return false;

        for (var i = 0; i < placedNodes.Count; i++)
        {
            var other = placedNodes[i];
            var minDist = radius + other.Radius + extraPadding;
            if (DistSq(x, y, other.Pos.X, other.Pos.Y) < minDist * minDist)
                return false;
        }

        return true;
    }

    private Node CreateNodeAt(double x, double y, double radius, double alpha = 1)
    {
        var speedMul = 0.92 + Random.Shared.NextDouble() * 0.5;
        var vx = (Random.Shared.NextDouble() - 0.45) * 10;
        var vy = Clamp(NextGaussian() * 8, -18, 18);

        var colorText = _settings.NodeColors[Random.Shared.Next(_settings.NodeColors.Length)];
        var fillColor = ParseHexColor(colorText, Colors.White);

        var node = new Node
        {
            Id = _nextId++,
            Pos = new Vector2D(x, y),
            Vel = new Vector2D(vx, vy),
            Radius = radius,
            Alpha = alpha,
            SpeedMul = speedMul,
            FillColor = fillColor,
            IconIndex = -1,
            Phase = Random.Shared.NextDouble() * 10,
            WobbleAmp = 6 + Random.Shared.NextDouble() * 10,
            WobbleFreq = 0.10 + Random.Shared.NextDouble() * 0.25
        };

        LimitNodeSpeed(node);
        AssignNodeIcon(node);
        return node;
    }

    private void UpdateMotion(double dt)
    {
        var movement = DirectionToVector(_settings.Direction);
        var baseDx = movement.X * _settings.ScrollSpeed * dt;
        var baseDy = movement.Y * _settings.ScrollSpeed * dt;

        for (var i = 0; i < _nodes.Count; i++)
        {
            var n = _nodes[i];

            n.Pos.X += baseDx * n.SpeedMul;
            n.Pos.Y += baseDy * n.SpeedMul;

            var wob = Math.Sin((n.Phase + _time * n.WobbleFreq) * Math.PI * 2) * n.WobbleAmp;

            if (movement.X != 0)
                n.Vel.Y += wob * dt;
            else
                n.Vel.X += wob * dt;

            n.Pos.X += n.Vel.X * dt;
            n.Pos.Y += n.Vel.Y * dt;

            LimitNodeSpeed(n);
            ApplyMagneticBounds(n, dt);
            n.Alpha = ComputeNodeAlpha(n);
        }
    }

    private void UpdateSpawning(double dt)
    {
        var rate = _settings.NodeSpawnRate;
        if (rate <= 0)
            return;

        _spawnAcc += dt * rate;
        var toSpawn = (int)Math.Floor(_spawnAcc);
        if (toSpawn <= 0)
            return;

        _spawnAcc -= toSpawn;

        for (var i = 0; i < toSpawn; i++)
        {
            if (_nodes.Count >= _settings.MaxNodes)
                break;

            var id = SpawnNode(true);
            _idToIndex[id] = _nodes.Count - 1;
            ConnectNewNode(id);
        }

        CompactEdges();
    }

    private int SpawnNode(bool spawnOutsideIngress)
    {
        var rect = GetMovementBoundsRect();
        var movement = DirectionToVector(_settings.Direction);
        var ingress = GetIngressPoint(rect, movement, spawnOutsideIngress);

        var x = ingress.X;
        var y = ingress.Y;
        var r = _settings.MinNodeRadius + Random.Shared.NextDouble() * (_settings.MaxNodeRadius - _settings.MinNodeRadius);

        if (movement.X != 0)
            y = rect.Top + r + 2 + Random.Shared.NextDouble() * Math.Max(1, rect.Height - r * 2 - 4);
        else
            x = rect.Left + r + 2 + Random.Shared.NextDouble() * Math.Max(1, rect.Width - r * 2 - 4);

        x += NextGaussian() * 16;
        y += NextGaussian() * 16;

        x = Clamp(x, rect.Left - 80, rect.Right + 80);
        y = Clamp(y, rect.Top - 80, rect.Bottom + 80);

        var colorText = _settings.NodeColors[Random.Shared.Next(_settings.NodeColors.Length)];
        var fillColor = ParseHexColor(colorText, Colors.White);

        var node = new Node
        {
            Id = _nextId++,
            Pos = new Vector2D(x, y),
            Vel = new Vector2D((Random.Shared.NextDouble() - 0.45) * 10, Clamp(NextGaussian() * 8, -18, 18)),
            Radius = r,
            Alpha = spawnOutsideIngress ? 0 : 1,
            SpeedMul = 0.92 + Random.Shared.NextDouble() * 0.5,
            FillColor = fillColor,
            IconIndex = -1,
            Phase = Random.Shared.NextDouble() * 10,
            WobbleAmp = 6 + Random.Shared.NextDouble() * 10,
            WobbleFreq = 0.10 + Random.Shared.NextDouble() * 0.25
        };

        LimitNodeSpeed(node);
        AssignNodeIcon(node);
        _nodes.Add(node);

        return node.Id;
    }

    private void AssignNodeIcon(Node node)
    {
        if (!_settings.DrawIcons)
        {
            node.IconIndex = -1;
            return;
        }

        if (_iconBitmaps.Count > 0 && (node.Radius * 2) >= _settings.IconMinDiameter)
            node.IconIndex = Random.Shared.Next(_iconBitmaps.Count);
        else
            node.IconIndex = -1;
    }

    private void CullExitedNodes()
    {
        var rect = GetMovementBoundsRect();
        var movement = DirectionToVector(_settings.Direction);
        var removedAny = false;

        for (var i = _nodes.Count - 1; i >= 0; i--)
        {
            var n = _nodes[i];
            if (!IsBeyondExitSide(n, rect, movement))
                continue;

            var id = n.Id;
            _nodes.RemoveAt(i);

            for (var e = _edges.Count - 1; e >= 0; e--)
            {
                if (_edges[e].A == id || _edges[e].B == id)
                    _edges.RemoveAt(e);
            }

            removedAny = true;
        }

        if (removedAny)
        {
            Reindex();
            CompactEdges();
        }
    }

    private bool IsBeyondExitSide(Node node, BoundsRect rect, Vector2D movement)
    {
        var pad = 30 + node.Radius;

        if (movement.X > 0) return node.Pos.X > rect.Right + pad;
        if (movement.X < 0) return node.Pos.X < rect.Left - pad;
        if (movement.Y > 0) return node.Pos.Y > rect.Bottom + pad;
        return node.Pos.Y < rect.Top - pad;
    }

    private double ComputeNodeAlpha(Node node)
    {
        var rect = GetMovementBoundsRect();
        var fadeIn = _settings.FadeIn;
        var fadeOut = _settings.FadeOut;

        double appearAlpha = 1;
        double disappearAlpha = 1;

        switch (_settings.Direction)
        {
            case AnimatedBackgroundDirection.Right:
                if (fadeIn > 0)
                    appearAlpha = Clamp((node.Pos.X - rect.Left) / fadeIn, 0, 1);

                if (fadeOut > 0)
                    disappearAlpha = Clamp((rect.Right - node.Pos.X) / fadeOut, 0, 1);
                break;

            case AnimatedBackgroundDirection.Left:
                if (fadeIn > 0)
                    appearAlpha = Clamp((rect.Right - node.Pos.X) / fadeIn, 0, 1);

                if (fadeOut > 0)
                    disappearAlpha = Clamp((node.Pos.X - rect.Left) / fadeOut, 0, 1);
                break;

            case AnimatedBackgroundDirection.Down:
                if (fadeIn > 0)
                    appearAlpha = Clamp((node.Pos.Y - rect.Top) / fadeIn, 0, 1);

                if (fadeOut > 0)
                    disappearAlpha = Clamp((rect.Bottom - node.Pos.Y) / fadeOut, 0, 1);
                break;

            case AnimatedBackgroundDirection.Up:
            default:
                if (fadeIn > 0)
                    appearAlpha = Clamp((rect.Bottom - node.Pos.Y) / fadeIn, 0, 1);

                if (fadeOut > 0)
                    disappearAlpha = Clamp((node.Pos.Y - rect.Top) / fadeOut, 0, 1);
                break;
        }

        return Clamp(Math.Min(appearAlpha, disappearAlpha), 0, 1);
    }

    private void ConnectNewNode(int newId)
    {
        var n = GetNodeById(newId);
        if (n is null || _nodes.Count < 2)
            return;

        var links = _settings.MinLinksPerNode;

        if (_settings.MaxLinksPerNode > _settings.MinLinksPerNode)
        {
            links = _settings.MinLinksPerNode +
                    Random.Shared.Next(_settings.MaxLinksPerNode - _settings.MinLinksPerNode + 1);
        }

        var best = new List<(int Index, double Dist)>();

        for (var i = 0; i < _nodes.Count; i++)
        {
            var other = _nodes[i];
            if (other.Id == newId)
                continue;

            var d = DistSq(n.Pos.X, n.Pos.Y, other.Pos.X, other.Pos.Y);
            InsertTopK(best, (i, d), 8);
        }

        var take = Math.Min(links, best.Count);

        for (var i = 0; i < take; i++)
            AddEdgeUnique(newId, _nodes[best[i].Index].Id);

        if (_settings.EnableExtraLink &&
            _nodes.Count > 3 &&
            _settings.ExtraLinkChance > 0 &&
            Random.Shared.NextDouble() < _settings.ExtraLinkChance)
        {
            var randomNode = _nodes[Random.Shared.Next(_nodes.Count)];
            if (randomNode.Id != newId)
                AddEdgeUnique(newId, randomNode.Id);
        }
    }

    private static void InsertTopK(List<(int Index, double Dist)> arr, (int Index, double Dist) item, int k)
    {
        var pos = arr.Count;

        for (var i = 0; i < arr.Count; i++)
        {
            if (item.Dist < arr[i].Dist)
            {
                pos = i;
                break;
            }
        }

        if (pos >= k)
            return;

        arr.Insert(pos, item);

        if (arr.Count > k)
            arr.RemoveAt(arr.Count - 1);
    }

    private void AddEdgeUnique(int a, int b)
    {
        if (a == b)
            return;

        if (a > b)
            (a, b) = (b, a);

        for (var i = 0; i < _edges.Count; i++)
        {
            var ea = _edges[i].A;
            var eb = _edges[i].B;

            if (ea > eb)
                (ea, eb) = (eb, ea);

            if (ea == a && eb == b)
                return;
        }

        _edges.Add(new Edge(a, b));
    }

    private void CompactEdges()
    {
        var maxEdges = Math.Max(30, _nodes.Count * 5);
        if (_edges.Count > maxEdges)
            _edges.RemoveRange(0, _edges.Count - maxEdges);
    }

    private void ApplyEdgeSprings(double dt)
    {
        if (_edges.Count == 0 || _nodes.Count < 2)
            return;

        const double edgeSpringK = 70.2;
        const double edgeSpringDamp = 0.001;
        const double edgeRestLen = 60;
        const double edgeLenPerRadius = 1.6;
        const double edgeMaxAccel = 300;
        const double edgeMaxForce = 399;

        for (var i = 0; i < _edges.Count; i++)
        {
            var ed = _edges[i];

            if (!_idToIndex.TryGetValue(ed.A, out var ia) ||
                !_idToIndex.TryGetValue(ed.B, out var ib) ||
                ia == ib)
            {
                continue;
            }

            var a = _nodes[ia];
            var b = _nodes[ib];

            var dx = b.Pos.X - a.Pos.X;
            var dy = b.Pos.Y - a.Pos.Y;
            var dist2 = dx * dx + dy * dy;

            if (dist2 < 1e-6)
                continue;

            var dist = Math.Sqrt(dist2);
            var nx = dx / dist;
            var ny = dy / dist;

            var rest = edgeRestLen + (a.Radius + b.Radius) * edgeLenPerRadius;
            var x = dist - rest;
            var rvx = b.Vel.X - a.Vel.X;
            var rvy = b.Vel.Y - a.Vel.Y;
            var relVelN = rvx * nx + rvy * ny;

            var force = Clamp((x * edgeSpringK) + (relVelN * edgeSpringDamp), -edgeMaxForce, edgeMaxForce);

            var ma = Clamp(a.Radius * a.Radius, 16, 900);
            var mb = Clamp(b.Radius * b.Radius, 16, 900);

            var aa = Clamp(force / ma, -edgeMaxAccel, edgeMaxAccel);
            var ab = Clamp(force / mb, -edgeMaxAccel, edgeMaxAccel);

            a.Vel.X += nx * aa * dt;
            a.Vel.Y += ny * aa * dt;
            b.Vel.X -= nx * ab * dt;
            b.Vel.Y -= ny * ab * dt;

            LimitNodeSpeed(a);
            LimitNodeSpeed(b);
        }
    }

    private void ApplyMagneticBounds(Node node, double dt)
    {
        var rect = GetMovementBoundsRect();
        var movement = DirectionToVector(_settings.Direction);
        var flow = GetFlowSides(movement);

        var left = rect.Left - _settings.InvisibleBouncePad + node.Radius;
        var right = rect.Right + _settings.InvisibleBouncePad - node.Radius;
        var top = rect.Top - _settings.InvisibleBouncePad + node.Radius;
        var bottom = rect.Bottom + _settings.InvisibleBouncePad - node.Radius;

        if (left > right) (left, right) = (right, left);
        if (top > bottom) (top, bottom) = (bottom, top);

        const double range = 60;
        const double boundMagK = 48;
        const double boundMagDamp = 0.01;
        const double boundMaxAccel = 380;

        double ax = 0;
        double ay = 0;

        var dLeft = node.Pos.X - left;
        var dRight = right - node.Pos.X;
        var dTop = node.Pos.Y - top;
        var dBottom = bottom - node.Pos.Y;

        var allowLeft = flow.Egress != "left";
        var allowRight = flow.Egress != "right";
        var allowTop = flow.Egress != "top";
        var allowBottom = flow.Egress != "bottom";

        if (allowLeft && dLeft < range)
        {
            var t = 1 - Clamp(dLeft / range, 0, 1);
            var s = t * t;
            ax += boundMagK * s;
            node.Vel.X *= 1 - (boundMagDamp * s * dt);
        }

        if (allowRight && dRight < range)
        {
            var t = 1 - Clamp(dRight / range, 0, 1);
            var s = t * t;
            ax -= boundMagK * s;
            node.Vel.X *= 1 - (boundMagDamp * s * dt);
        }

        if (allowTop && dTop < range)
        {
            var t = 1 - Clamp(dTop / range, 0, 1);
            var s = t * t;
            ay += boundMagK * s;
            node.Vel.Y *= 1 - (boundMagDamp * s * dt);
        }

        if (allowBottom && dBottom < range)
        {
            var t = 1 - Clamp(dBottom / range, 0, 1);
            var s = t * t;
            ay -= boundMagK * s;
            node.Vel.Y *= 1 - (boundMagDamp * s * dt);
        }

        if (allowLeft && dLeft < 0)
            ax += boundMagK * (1.5 + Clamp((-dLeft) / 8, 0, 4));
        else if (allowRight && dRight < 0)
            ax -= boundMagK * (1.5 + Clamp((-dRight) / 8, 0, 4));

        if (allowTop && dTop < 0)
            ay += boundMagK * (1.5 + Clamp((-dTop) / 8, 0, 4));
        else if (allowBottom && dBottom < 0)
            ay -= boundMagK * (1.5 + Clamp((-dBottom) / 8, 0, 4));

        ax = Clamp(ax, -boundMaxAccel, boundMaxAccel);
        ay = Clamp(ay, -boundMaxAccel, boundMaxAccel);

        node.Vel.X += ax * dt;
        node.Vel.Y += ay * dt;

        LimitNodeSpeed(node);

        if (allowLeft && node.Pos.X < left) node.Pos.X = left;
        if (allowRight && node.Pos.X > right) node.Pos.X = right;
        if (allowTop && node.Pos.Y < top) node.Pos.Y = top;
        if (allowBottom && node.Pos.Y > bottom) node.Pos.Y = bottom;
    }

    private void ResolveOverlaps()
    {
        var rect = GetMovementBoundsRect();
        var cellSize = Math.Max(8, _settings.MaxNodeRadius * 2 + _settings.CollisionPadding * 2);
        var iterations = _settings.CollisionIterations;
        var dt = Math.Max(1e-4, _dtLast);
        var invDt = 1.0 / dt;

        for (var it = 0; it < iterations; it++)
        {
            BuildGrid(cellSize);

            for (var i = 0; i < _nodes.Count; i++)
            {
                var ni = _nodes[i];
                var cell = CellOf(ni.Pos, cellSize);

                for (var oy = -1; oy <= 1; oy++)
                {
                    for (var ox = -1; ox <= 1; ox++)
                    {
                        var key = Pack(cell.Cx + ox, cell.Cy + oy);

                        if (!_grid.TryGetValue(key, out var list))
                            continue;

                        for (var li = 0; li < list.Count; li++)
                        {
                            var j = list[li];
                            if (j <= i)
                                continue;

                            var nj = _nodes[j];
                            var dx = nj.Pos.X - ni.Pos.X;
                            var dy = nj.Pos.Y - ni.Pos.Y;
                            var d2 = dx * dx + dy * dy;

                            var minDist = ni.Radius + nj.Radius + _settings.CollisionPadding;
                            var minDist2 = minDist * minDist;

                            if (d2 >= minDist2)
                                continue;

                            double dist;
                            double nx;
                            double ny;

                            if (d2 < 1e-6)
                            {
                                var a = (ni.Id * 12.9898 + nj.Id * 78.233) * 0.0174533;
                                nx = Math.Cos(a);
                                ny = Math.Sin(a);
                                dist = 0.001;
                            }
                            else
                            {
                                dist = Math.Sqrt(d2);
                                nx = dx / dist;
                                ny = dy / dist;
                            }

                            var penetration = minDist - dist;
                            penetration = Math.Max(0, penetration - _settings.CollisionSlopPx);

                            if (penetration <= 0)
                                continue;

                            var corr = penetration * 0.5 * _settings.CollisionPositionCorrection;
                            corr = Math.Min(corr, _settings.CollisionMaxCorrectionPx);

                            var posDeltaX = nx * corr;
                            var posDeltaY = ny * corr;

                            ni.Pos.X -= posDeltaX;
                            ni.Pos.Y -= posDeltaY;
                            nj.Pos.X += posDeltaX;
                            nj.Pos.Y += posDeltaY;

                            var rvx = nj.Vel.X - ni.Vel.X;
                            var rvy = nj.Vel.Y - ni.Vel.Y;
                            var relN = rvx * nx + rvy * ny;
                            var approaching = relN < 0;
                            var targetSepVel = penetration * _settings.CollisionSeparationSpring * invDt;

                            var impulseN = approaching
                                ? (-relN + targetSepVel) * _settings.CollisionImpulseDamping
                                : (targetSepVel * 0.25);

                            var impX = nx * impulseN * 0.5;
                            var impY = ny * impulseN * 0.5;

                            ni.Vel.X -= impX;
                            ni.Vel.Y -= impY;
                            nj.Vel.X += impX;
                            nj.Vel.Y += impY;

                            var dot = rvx * nx + rvy * ny;
                            var tx = rvx - nx * dot;
                            var ty = rvy - ny * dot;
                            var tLen = Math.Sqrt(tx * tx + ty * ty);

                            if (tLen > 1e-4)
                            {
                                var tnx = tx / tLen;
                                var tny = ty / tLen;
                                var jt = tLen * _settings.CollisionTangentFriction;
                                var ftX = tnx * jt * 0.5;
                                var ftY = tny * jt * 0.5;

                                ni.Vel.X += ftX;
                                ni.Vel.Y += ftY;
                                nj.Vel.X -= ftX;
                                nj.Vel.Y -= ftY;
                            }

                            LimitNodeSpeed(ni);
                            LimitNodeSpeed(nj);
                            ClampNodeForCollision(ni, rect);
                            ClampNodeForCollision(nj, rect);
                        }
                    }
                }
            }
        }
    }

    private void BuildGrid(double cellSize)
    {
        _grid.Clear();

        for (var i = 0; i < _nodes.Count; i++)
        {
            var c = CellOf(_nodes[i].Pos, cellSize);
            var key = Pack(c.Cx, c.Cy);

            if (!_grid.TryGetValue(key, out var list))
            {
                list = new List<int>(4);
                _grid[key] = list;
            }

            list.Add(i);
        }
    }

    private static (int Cx, int Cy) CellOf(Vector2D p, double cellSize)
        => ((int)Math.Floor(p.X / cellSize), (int)Math.Floor(p.Y / cellSize));

    private static long Pack(int x, int y)
        => ((long)x << 32) ^ (uint)y;

    private void Reindex()
    {
        _idToIndex.Clear();

        for (var i = 0; i < _nodes.Count; i++)
            _idToIndex[_nodes[i].Id] = i;
    }

    private Node? GetNodeById(int id)
    {
        if (!_idToIndex.TryGetValue(id, out var idx))
            return null;

        return idx >= 0 && idx < _nodes.Count ? _nodes[idx] : null;
    }

    private BoundsRect GetMovementBoundsRect()
    {
        return new BoundsRect(
            _settings.BoundsLeft,
            _settings.BoundsTop,
            _layerWidthDip - _settings.BoundsRight,
            _layerHeightDip - _settings.BoundsBottom);
    }

    private static Vector2D DirectionToVector(AnimatedBackgroundDirection direction)
    {
        return direction switch
        {
            AnimatedBackgroundDirection.Up => new Vector2D(0, -1),
            AnimatedBackgroundDirection.Right => new Vector2D(1, 0),
            AnimatedBackgroundDirection.Down => new Vector2D(0, 1),
            _ => new Vector2D(-1, 0)
        };
    }

    private static (string Ingress, string Egress) GetFlowSides(Vector2D movement)
    {
        if (movement.X > 0) return ("left", "right");
        if (movement.X < 0) return ("right", "left");
        if (movement.Y > 0) return ("top", "bottom");
        return ("bottom", "top");
    }

    private static Vector2D GetIngressPoint(BoundsRect rect, Vector2D movement, bool spawnOutside)
    {
        var pad = spawnOutside ? 20 + Random.Shared.NextDouble() * 80 : 0;

        if (movement.X > 0) return new Vector2D(rect.Left - pad, rect.Top + rect.Height * 0.5);
        if (movement.X < 0) return new Vector2D(rect.Right + pad, rect.Top + rect.Height * 0.5);
        if (movement.Y > 0) return new Vector2D(rect.Left + rect.Width * 0.5, rect.Top - pad);
        return new Vector2D(rect.Left + rect.Width * 0.5, rect.Bottom + pad);
    }

    private void ClampNodeForCollision(Node node, BoundsRect rect)
    {
        var movement = DirectionToVector(_settings.Direction);
        var flow = GetFlowSides(movement);

        node.Pos.X = Clamp(node.Pos.X, rect.Left - 300, rect.Right + 300);
        node.Pos.Y = Clamp(node.Pos.Y, rect.Top - 300, rect.Bottom + 300);

        if (flow.Egress != "left" && node.Pos.X < rect.Left) node.Pos.X = rect.Left;
        if (flow.Egress != "right" && node.Pos.X > rect.Right) node.Pos.X = rect.Right;
        if (flow.Egress != "top" && node.Pos.Y < rect.Top) node.Pos.Y = rect.Top;
        if (flow.Egress != "bottom" && node.Pos.Y > rect.Bottom) node.Pos.Y = rect.Bottom;
    }

    private void LimitNodeSpeed(Node node)
    {
        var max = _settings.MaxNodeSpeed;
        var vx = node.Vel.X;
        var vy = node.Vel.Y;
        var speedSq = vx * vx + vy * vy;
        var maxSq = max * max;

        if (speedSq <= maxSq)
            return;

        var speed = Math.Sqrt(speedSq);
        if (speed <= 0.0001)
            return;

        var scale = max / speed;
        node.Vel.X *= scale;
        node.Vel.Y *= scale;
    }

    private Particle RentParticle(double x, double y)
    {
        var particle = _particlePool.Count > 0 ? _particlePool.Pop() : new Particle();
        particle.Reset(x, y);
        return particle;
    }

    private Rocket RentRocket(double x, double y)
    {
        var rocket = _rocketPool.Count > 0 ? _rocketPool.Pop() : new Rocket();
        rocket.Reset(x, y);
        return rocket;
    }

    private void RecycleParticle(Particle particle)
    {
        particle.ResetForPool();

        if (_particlePool.Count < MaxParticlePoolSize)
            _particlePool.Push(particle);
    }

    private void RecycleRocket(Rocket rocket)
    {
        rocket.ResetForPool();

        if (_rocketPool.Count < MaxRocketPoolSize)
            _rocketPool.Push(rocket);
    }

    private void RecycleAllParticles()
    {
        for (var i = 0; i < _particles.Count; i++)
            RecycleParticle(_particles[i]);

        _particles.Clear();
    }

    private void RecycleAllRockets()
    {
        for (var i = 0; i < _rockets.Count; i++)
            RecycleRocket(_rockets[i]);

        _rockets.Clear();
    }

    private static double DistSq(double ax, double ay, double bx, double by)
    {
        var dx = bx - ax;
        var dy = by - ay;
        return dx * dx + dy * dy;
    }

    private static double NextGaussian()
    {
        double u = 0;
        double v = 0;

        while (u == 0) u = Random.Shared.NextDouble();
        while (v == 0) v = Random.Shared.NextDouble();

        return Math.Sqrt(-2.0 * Math.Log(u)) * Math.Cos(2.0 * Math.PI * v);
    }

    private static double Clamp(double value, double min, double max)
        => Math.Max(min, Math.Min(max, value));

    private static bool UsesEffectBlend(AnimatedBackgroundBlendMode mode)
        => mode is AnimatedBackgroundBlendMode.Multiply
            or AnimatedBackgroundBlendMode.Screen
            or AnimatedBackgroundBlendMode.Overlay;

    private static CanvasBlend ToCanvasBlend(AnimatedBackgroundBlendMode mode)
        => mode switch
        {
            AnimatedBackgroundBlendMode.Lighter => CanvasBlend.Add,
            _ => CanvasBlend.SourceOver
        };

    private static BlendEffectMode ToBlendEffectMode(AnimatedBackgroundBlendMode mode)
        => mode switch
        {
            AnimatedBackgroundBlendMode.Multiply => BlendEffectMode.Multiply,
            AnimatedBackgroundBlendMode.Screen => BlendEffectMode.Screen,
            AnimatedBackgroundBlendMode.Overlay => BlendEffectMode.Overlay,
            _ => BlendEffectMode.Multiply
        };

    private static AnimatedBackgroundAnimationSettings NormalizeSettings(AnimatedBackgroundAnimationSettings s)
    {
        var x = CloneSettings(s);

        x.MaxNodes = Math.Max(0, x.MaxNodes);
        x.MaxNodeSpeed = Math.Max(1, x.MaxNodeSpeed);
        x.CollisionIterations = (int)Clamp(x.CollisionIterations, 0, 8);
        x.IconScale = Clamp(x.IconScale, 0.05, 2);
        x.NodesOpacity = Clamp(x.NodesOpacity, 0, 4);
        x.PulseIconWidthAmplitude = Clamp(x.PulseIconWidthAmplitude, 0, 0.6);
        x.PulseIconWidthFrequency = Clamp(x.PulseIconWidthFrequency, 0.1, 20);
        x.BounceDamping = Clamp(x.BounceDamping, 0, 1);
        x.CollisionPositionCorrection = Clamp(x.CollisionPositionCorrection, 0, 1);
        x.CollisionSlopPx = Clamp(x.CollisionSlopPx, 0, 4);
        x.CollisionMaxCorrectionPx = Clamp(x.CollisionMaxCorrectionPx, 0, 50);
        x.CollisionSeparationSpring = Clamp(x.CollisionSeparationSpring, 0, 25);
        x.CollisionImpulseDamping = Clamp(x.CollisionImpulseDamping, 0, 1);
        x.CollisionTangentFriction = Clamp(x.CollisionTangentFriction, 0, 0.5);
        x.MinLinksPerNode = (int)Clamp(x.MinLinksPerNode, 0, 20);
        x.MaxLinksPerNode = (int)Clamp(x.MaxLinksPerNode, 0, 20);
        x.ExtraLinkChance = Clamp(x.ExtraLinkChance, 0, 1);
        x.FireworksOpacity = Clamp(x.FireworksOpacity, 0, 1);
        x.FireworksFadeFillAlpha = Clamp(x.FireworksFadeFillAlpha, 0, 1);
        x.FireworksMaxParticles = Math.Max(0, x.FireworksMaxParticles);
        x.FireworksMaxRockets = Math.Max(0, x.FireworksMaxRockets);
        x.FireworksLaunchIntervalMs = Math.Max(50, x.FireworksLaunchIntervalMs);

        if (x.MaxNodeRadius < x.MinNodeRadius)
            x.MaxNodeRadius = x.MinNodeRadius;

        if (x.MaxLinksPerNode < x.MinLinksPerNode)
            x.MaxLinksPerNode = x.MinLinksPerNode;

        x.NodeColors ??=
        [
            "#C5CFD4", "#C5CFD4", "#C5CFD4", "#C5CFD4", "#C5CFD4",
            "#C5CFD4", "#C5CFD4", "#F6D988", "#F6D988", "#F6D988"
        ];

        if (x.NodeColors.Length == 0)
        {
            x.NodeColors =
            [
                "#C5CFD4", "#C5CFD4", "#C5CFD4", "#C5CFD4", "#C5CFD4",
                "#C5CFD4", "#C5CFD4", "#F6D988", "#F6D988", "#F6D988"
            ];
        }

        x.IconUris ??= [];
        x.FireworksColors ??= [];

        return x;
    }

    private static bool TryParseCssColor(string? text, out Color color)
    {
        color = Colors.Transparent;

        if (string.IsNullOrWhiteSpace(text))
            return false;

        var value = text.Trim();

        if (string.Equals(value, "transparent", StringComparison.OrdinalIgnoreCase))
        {
            color = Colors.Transparent;
            return true;
        }

        if (!value.StartsWith("#"))
            return false;

        var hex = value[1..];

        if (hex.Length == 3)
        {
            var r = Convert.ToByte(new string(hex[0], 2), 16);
            var g = Convert.ToByte(new string(hex[1], 2), 16);
            var b = Convert.ToByte(new string(hex[2], 2), 16);
            color = Color.FromArgb(255, r, g, b);
            return true;
        }

        if (hex.Length == 6)
        {
            var r = Convert.ToByte(hex.Substring(0, 2), 16);
            var g = Convert.ToByte(hex.Substring(2, 2), 16);
            var b = Convert.ToByte(hex.Substring(4, 2), 16);
            color = Color.FromArgb(255, r, g, b);
            return true;
        }

        if (hex.Length == 8)
        {
            var a = Convert.ToByte(hex.Substring(0, 2), 16);
            var r = Convert.ToByte(hex.Substring(2, 2), 16);
            var g = Convert.ToByte(hex.Substring(4, 2), 16);
            var b = Convert.ToByte(hex.Substring(6, 2), 16);
            color = Color.FromArgb(a, r, g, b);
            return true;
        }

        return false;
    }

    private static Color ParseHexColor(string? hex, Color fallback)
        => TryParseCssColor(hex, out var color) ? color : fallback;

    private static Color WithAlpha(Color color, double alpha01)
        => Color.FromArgb(
            (byte)Math.Round(Clamp(alpha01, 0, 1) * 255),
            color.R,
            color.G,
            color.B);

    private static Color HueToColor(double hue, double alpha)
    {
        hue %= 360;
        if (hue < 0) hue += 360;

        var c = 1.0;
        var x = c * (1 - Math.Abs((hue / 60.0) % 2 - 1));

        double r = 0;
        double g = 0;
        double b = 0;

        if (hue < 60) { r = c; g = x; }
        else if (hue < 120) { r = x; g = c; }
        else if (hue < 180) { g = c; b = x; }
        else if (hue < 240) { g = x; b = c; }
        else if (hue < 300) { r = x; b = c; }
        else { r = c; b = x; }

        return Color.FromArgb(
            (byte)Math.Round(Clamp(alpha, 0, 1) * 255),
            (byte)Math.Round(r * 255),
            (byte)Math.Round(g * 255),
            (byte)Math.Round(b * 255));
    }

    private void DisposeBitmaps()
    {
        _backgroundBitmap?.Dispose();
        _backgroundBitmap = null;

        for (var i = 0; i < _iconBitmaps.Count; i++)
            _iconBitmaps[i].Dispose();

        _iconBitmaps.Clear();
    }

    private void DisposeResources()
    {
        DisposeBitmaps();

        RecycleAllParticles();
        RecycleAllRockets();

        _particlePool.Clear();
        _rocketPool.Clear();

        _sceneLayer?.Dispose();
        _sceneLayer = null;

        _iconLayer?.Dispose();
        _iconLayer = null;

        _fireworksLayer?.Dispose();
        _fireworksLayer = null;
    }

    public void Dispose()
    {
        if (_isRunning)
            CompositionTarget.Rendering -= CompositionTarget_Rendering;

        DisposeResources();
    }

    private sealed class StopwatchEx
    {
        private readonly System.Diagnostics.Stopwatch _sw = new();

        public void Restart() => _sw.Restart();
        public double ElapsedTotalSeconds => _sw.Elapsed.TotalSeconds;
    }

    private sealed class Vector2D
    {
        public double X;
        public double Y;

        public Vector2D()
        {
        }

        public Vector2D(double x, double y)
        {
            X = x;
            Y = y;
        }
    }

    private sealed class Node
    {
        public int Id { get; set; }
        public Vector2D Pos { get; set; } = new();
        public Vector2D Vel { get; set; } = new();
        public double Radius { get; set; }
        public double Alpha { get; set; }
        public double SpeedMul { get; set; }
        public Color FillColor { get; set; } = Colors.White;
        public int IconIndex { get; set; }
        public double Phase { get; set; }
        public double WobbleAmp { get; set; }
        public double WobbleFreq { get; set; }
    }

    private readonly record struct Edge(int A, int B);

    private readonly record struct BoundsRect(double Left, double Top, double Right, double Bottom)
    {
        public double Width => Right - Left;
        public double Height => Bottom - Top;
    }

    private class Particle
    {
        public double X;
        public double Y;
        public double Vx;
        public double Vy;

        public double Shrink = 0.97;
        public double Size = 2;
        public double Resistance = 1;
        public double Gravity = 0;
        public bool Flick;
        public double Alpha = 1;
        public double Fade;
        public bool HasColor;
        public Color Color = Colors.White;
        public double Hue;

        public virtual void Reset(double x, double y)
        {
            X = x;
            Y = y;
            Vx = 0;
            Vy = 0;
            Shrink = 0.97;
            Size = 2;
            Resistance = 1;
            Gravity = 0;
            Flick = false;
            Alpha = 1;
            Fade = 0;
            HasColor = false;
            Color = Colors.White;
            Hue = 0;
        }

        public virtual void ResetForPool()
        {
            X = 0;
            Y = 0;
            Vx = 0;
            Vy = 0;
            Shrink = 0.97;
            Size = 2;
            Resistance = 1;
            Gravity = 0;
            Flick = false;
            Alpha = 1;
            Fade = 0;
            HasColor = false;
            Color = Colors.White;
            Hue = 0;
        }

        public virtual void Update()
        {
            Vx *= Resistance;
            Vy *= Resistance;
            Vy += Gravity;
            X += Vx;
            Y += Vy;
            Size *= Shrink;
            Alpha -= Fade;
        }

        public bool Exists()
            => Alpha >= 0.08 && Size >= 0.7;

        public virtual void Render(CanvasDrawingSession ds, double opacity)
        {
            if (!Exists())
                return;

            var effectiveAlpha = Clamp(Alpha * opacity, 0, 1);
            if (effectiveAlpha <= 0.01)
                return;

            var drawRadius = Flick
                ? Math.Max(0.3, Size * (0.45 + Random.Shared.NextDouble() * 0.55))
                : Math.Max(0.3, Size);

            var baseColor = HasColor ? Color : HueToColor(Hue, 1);

            var oldBlend = ds.Blend;
            ds.Blend = CanvasBlend.Add;

            ds.FillCircle(
                (float)X,
                (float)Y,
                (float)drawRadius,
                WithAlpha(baseColor, effectiveAlpha * 0.55));

            ds.FillCircle(
                (float)X,
                (float)Y,
                (float)Math.Max(0.2, drawRadius * 0.40),
                WithAlpha(Colors.White, effectiveAlpha * 0.9));

            ds.Blend = oldBlend;
        }
    }

    private sealed class Rocket : Particle
    {
        public bool HasExplosionColor;
        public Color ExplosionColor = Colors.White;
        public double ExplosionHue;

        public override void Reset(double x, double y)
        {
            base.Reset(x, y);
            HasExplosionColor = false;
            ExplosionColor = Colors.White;
            ExplosionHue = 0;
        }

        public override void ResetForPool()
        {
            base.ResetForPool();
            HasExplosionColor = false;
            ExplosionColor = Colors.White;
            ExplosionHue = 0;
        }

        public void Explode(AnimatedBackground owner)
        {
            var extra = Math.Max(0, owner._settings.FireworksExplosionExtraParticles);
            var count = owner._settings.FireworksExplosionMinParticles + Random.Shared.Next(extra + 1);

            for (var i = 0; i < count; i++)
            {
                var particle = owner.RentParticle(X, Y);

                var angle = Random.Shared.NextDouble() * Math.PI * 2;
                var speed = Math.Cos(Random.Shared.NextDouble() * Math.PI / 2) * 15;

                particle.Vx = Math.Cos(angle) * speed;
                particle.Vy = Math.Sin(angle) * speed;
                particle.Size = owner._settings.FireworksParticleSize;
                particle.Gravity = owner._settings.FireworksParticleGravity;
                particle.Resistance = owner._settings.FireworksParticleResistance;
                particle.Shrink = Random.Shared.NextDouble() * owner._settings.FireworksParticleShrinkRange
                                  + owner._settings.FireworksParticleShrinkMin;
                particle.Flick = owner._settings.FireworksParticleFlicker;
                particle.Fade = 0.015 + Random.Shared.NextDouble() * 0.02;

                if (HasExplosionColor)
                {
                    particle.HasColor = true;
                    particle.Color = ExplosionColor;
                }
                else
                {
                    particle.HasColor = false;
                    particle.Hue = ExplosionHue;
                }

                owner._particles.Add(particle);
            }
        }

        public override void Render(CanvasDrawingSession ds, double opacity)
        {
            if (!Exists())
                return;

            var effectiveAlpha = Clamp(Alpha * opacity, 0, 1);
            if (effectiveAlpha <= 0.01)
                return;

            var drawRadius = Flick
                ? Math.Max(0.5, Size * (0.55 + Random.Shared.NextDouble() * 0.45))
                : Math.Max(0.5, Size);

            var oldBlend = ds.Blend;
            ds.Blend = CanvasBlend.Add;

            ds.FillCircle(
                (float)X,
                (float)Y,
                (float)drawRadius,
                WithAlpha(Color.FromArgb(255, 255, 215, 0), effectiveAlpha * 0.65));

            ds.FillCircle(
                (float)X,
                (float)Y,
                (float)Math.Max(0.25, drawRadius * 0.45),
                WithAlpha(Colors.White, effectiveAlpha));

            ds.Blend = oldBlend;
        }
    }
}