using System;
using System.Collections.Generic;
using Windows.Foundation;

namespace Sonex.Client.Controls.SceneCanvas;

internal sealed class SceneSpatialIndex
{
    private readonly Dictionary<long, List<SceneCanvasObject>> _cells = [];
    private readonly Dictionary<SceneCanvasObject, List<long>> _objectCells = [];
    private readonly float _cellSizeMeters;

    public SceneSpatialIndex(float cellSizeMeters)
    {
        _cellSizeMeters = Math.Max(0.1f, cellSizeMeters);
    }

    public void Clear()
    {
        _cells.Clear();
        _objectCells.Clear();
    }

    public void Add(SceneCanvasObject sceneObject)
    {
        Update(sceneObject);
    }

    public bool Remove(SceneCanvasObject sceneObject)
    {
        if (sceneObject == null)
        {
            return false;
        }

        if (!_objectCells.TryGetValue(sceneObject, out var keys))
        {
            return false;
        }

        foreach (var key in keys)
        {
            if (_cells.TryGetValue(key, out var list))
            {
                list.Remove(sceneObject);
                if (list.Count == 0)
                {
                    _cells.Remove(key);
                }
            }
        }

        _objectCells.Remove(sceneObject);
        return true;
    }

    public void Update(SceneCanvasObject sceneObject)
    {
        if (sceneObject == null)
        {
            return;
        }

        var keys = CollectKeys(sceneObject.SpatialBoundsMeters);
        if (_objectCells.TryGetValue(sceneObject, out var existingKeys))
        {
            if (HaveSameKeys(existingKeys, keys))
            {
                return;
            }

            RemoveFromCells(sceneObject, existingKeys);
        }

        _objectCells[sceneObject] = keys;
        AddToCells(sceneObject, keys);
    }

    public void Query(Rect worldBoundsMeters, List<SceneCanvasObject> destination, int queryToken)
    {
        if (destination == null)
        {
            return;
        }

        if (_cells.Count == 0)
        {
            return;
        }

        var minX = ToCellX(worldBoundsMeters.X);
        var minY = ToCellY(worldBoundsMeters.Y);
        var maxX = ToCellX(worldBoundsMeters.X + worldBoundsMeters.Width);
        var maxY = ToCellY(worldBoundsMeters.Y + worldBoundsMeters.Height);

        for (int y = minY; y <= maxY; y++)
        {
            for (int x = minX; x <= maxX; x++)
            {
                var key = MakeKey(x, y);
                if (!_cells.TryGetValue(key, out var list))
                {
                    continue;
                }

                foreach (var sceneObject in list)
                {
                    if (sceneObject.LastQueryToken == queryToken)
                    {
                        continue;
                    }

                    sceneObject.LastQueryToken = queryToken;

                    if (!sceneObject.IsVisible)
                    {
                        continue;
                    }

                    if (!Intersects(worldBoundsMeters, sceneObject.SpatialBoundsMeters))
                    {
                        continue;
                    }

                    destination.Add(sceneObject);
                }
            }
        }
    }

    private List<long> CollectKeys(Rect boundsMeters)
    {
        var normalized = NormalizeRect(boundsMeters);
        var minX = ToCellX(normalized.X);
        var minY = ToCellY(normalized.Y);
        var maxX = ToCellX(normalized.X + normalized.Width);
        var maxY = ToCellY(normalized.Y + normalized.Height);

        var keys = new List<long>((maxX - minX + 1) * (maxY - minY + 1));

        for (int y = minY; y <= maxY; y++)
        {
            for (int x = minX; x <= maxX; x++)
            {
                keys.Add(MakeKey(x, y));
            }
        }

        return keys;
    }

    private void AddToCells(SceneCanvasObject sceneObject, List<long> keys)
    {
        foreach (var key in keys)
        {
            if (!_cells.TryGetValue(key, out var list))
            {
                list = [];
                _cells[key] = list;
            }

            list.Add(sceneObject);
        }
    }

    private void RemoveFromCells(SceneCanvasObject sceneObject, List<long> keys)
    {
        foreach (var key in keys)
        {
            if (!_cells.TryGetValue(key, out var list))
            {
                continue;
            }

            list.Remove(sceneObject);
            if (list.Count == 0)
            {
                _cells.Remove(key);
            }
        }
    }

    private static bool HaveSameKeys(List<long> currentKeys, List<long> nextKeys)
    {
        if (ReferenceEquals(currentKeys, nextKeys))
        {
            return true;
        }

        if (currentKeys == null || nextKeys == null || currentKeys.Count != nextKeys.Count)
        {
            return false;
        }

        for (var i = 0; i < currentKeys.Count; i++)
        {
            if (currentKeys[i] != nextKeys[i])
            {
                return false;
            }
        }

        return true;
    }

    private int ToCellX(double x)
    {
        return (int)Math.Floor(x / _cellSizeMeters);
    }

    private int ToCellY(double y)
    {
        return (int)Math.Floor(y / _cellSizeMeters);
    }

    private static long MakeKey(int x, int y)
    {
        return ((long)x << 32) ^ (uint)y;
    }

    private static bool Intersects(Rect a, Rect b)
    {
        return a.X < b.X + b.Width &&
               a.X + a.Width > b.X &&
               a.Y < b.Y + b.Height &&
               a.Y + a.Height > b.Y;
    }

    private static Rect NormalizeRect(Rect rect)
    {
        var x = rect.X;
        var y = rect.Y;
        var width = rect.Width;
        var height = rect.Height;

        if (width < 0d)
        {
            x += width;
            width = -width;
        }

        if (height < 0d)
        {
            y += height;
            height = -height;
        }

        return new Rect(x, y, Math.Max(0d, width), Math.Max(0d, height));
    }
}
