using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using HG;
using Unity.Collections;
using UnityEngine;

namespace RoR2.Navigation;

public class BlockMap<TItem, TItemPositionGetter> where TItemPositionGetter : IPosition3Getter<TItem>
{
	private struct ItemDistanceSqrPair
	{
		public int itemIndex;

		public float distanceSqr;
	}

	private interface ISearchResultHandler
	{
		bool OnEncounterResult(TItem result);
	}

	private struct SingleSearchResultHandler : ISearchResultHandler
	{
		public bool foundResult { get; private set; }

		public TItem result { get; private set; }

		public bool OnEncounterResult(TItem result)
		{
			foundResult = true;
			this.result = result;
			return false;
		}
	}

	private struct ListWriteSearchResultHandler : ISearchResultHandler
	{
		private readonly List<TItem> dest;

		public ListWriteSearchResultHandler(List<TItem> dest)
		{
			this.dest = dest;
		}

		public bool OnEncounterResult(TItem result)
		{
			dest.Add(result);
			return true;
		}
	}

	private struct GridEnumerator
	{
		private readonly Vector3Int startPos;

		private readonly Vector3Int endPos;

		private Vector3Int _current;

		public Vector3Int Current => _current;

		public GridEnumerator(in Vector3Int startCellIndex, in Vector3Int endCellIndex)
		{
			startPos = startCellIndex;
			endPos = endCellIndex;
			_current = startCellIndex;
			ref Vector3Int current = ref _current;
			int x = current.x - 1;
			current.x = x;
		}

		public bool MoveNext()
		{
			if (++_current.x >= endPos.x)
			{
				_current.x = startPos.x;
				if (++_current.z >= endPos.z)
				{
					_current.z = startPos.z;
					if (++_current.y >= endPos.y)
					{
						_current.y = startPos.y;
						return false;
					}
				}
			}
			return true;
		}

		public void Reset()
		{
			_current = startPos;
		}
	}

	private struct GridEnumerable
	{
		private readonly Vector3Int startPos;

		private readonly Vector3Int endPos;

		public GridEnumerable(Vector3Int startPos, Vector3Int endPos)
		{
			this.startPos = startPos;
			this.endPos = endPos;
		}

		public GridEnumerator GetEnumerator()
		{
			return new GridEnumerator(in startPos, in endPos);
		}
	}

	private const bool debugDraw = false;

	private const bool aggressiveDebug = false;

	private Vector3 cellSize;

	private Vector3 invCellSize;

	private Bounds worldBoundingBox;

	private BlockMapCell[] cells = Array.Empty<BlockMapCell>();

	private int cellCount1D;

	private Vector3Int cellCounts;

	private TItem[] itemsPackedByCell = Array.Empty<TItem>();

	private int itemCount;

	private TItemPositionGetter itemPositionGetter;

	public BlockMap()
		: this(new Vector3(15f, 30f, 15f))
	{
	}

	public BlockMap(Vector3 cellSize)
	{
		SetCellSize(cellSize);
	}

	public void Reset()
	{
		worldBoundingBox = default(Bounds);
		ArrayUtils.Clear(itemsPackedByCell, ref itemCount);
		cellCounts = Vector3Int.zero;
		cellCount1D = 0;
	}

	public void SetCellSize(Vector3 newSize)
	{
		if (cellSize != newSize)
		{
			cellSize = newSize;
			invCellSize = new Vector3(1f / cellSize.x, 1f / cellSize.y, 1f / cellSize.z);
			Reset();
		}
	}

	public void Set<T>(T newItems, int newItemsLength, TItemPositionGetter newItemPositionGetter) where T : IList<TItem>
	{
		Reset();
		NativeArray<BlockMapCellIndex> nativeArray = new NativeArray<BlockMapCellIndex>(newItemsLength, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
		try
		{
			itemPositionGetter = newItemPositionGetter;
			if (newItems.Count > 0)
			{
				worldBoundingBox = new Bounds(itemPositionGetter.GetPosition3(newItems[0]), Vector3.zero);
				for (int i = 1; i < newItems.Count; i++)
				{
					worldBoundingBox.Encapsulate(itemPositionGetter.GetPosition3(newItems[i]));
				}
			}
			worldBoundingBox.min -= Vector3.one;
			worldBoundingBox.max += Vector3.one;
			Vector3 size = worldBoundingBox.size;
			cellCounts = Vector3Int.Max(Vector3Int.CeilToInt(Vector3.Scale(size, invCellSize)), Vector3Int.one);
			cellCount1D = cellCounts.x * cellCounts.y * cellCounts.z;
			Array.Resize(ref cells, cellCount1D);
			Array.Clear(cells, 0, cells.Length);
			_ = worldBoundingBox.min;
			for (int j = 0; j < newItems.Count; j++)
			{
				Vector3 worldPosition = itemPositionGetter.GetPosition3(newItems[j]);
				Vector3Int gridPos = WorldPositionToGridPosFloor(in worldPosition);
				BlockMapCellIndex blockMapCellIndex2 = (nativeArray[j] = GridPosToCellIndex(in gridPos));
				cells[(int)blockMapCellIndex2].itemCount++;
			}
			int num = 0;
			for (int k = 0; k < cells.Length; k++)
			{
				ref BlockMapCell reference = ref cells[k];
				reference.itemStartIndex = num;
				num += reference.itemCount;
			}
			itemCount = newItems.Count;
			ArrayUtils.EnsureCapacity(ref itemsPackedByCell, itemCount);
			NativeArray<int> nativeArray2 = new NativeArray<int>(cells.Length, Allocator.Temp);
			for (int l = 0; l < itemCount; l++)
			{
				BlockMapCellIndex blockMapCellIndex3 = nativeArray[l];
				ref BlockMapCell reference2 = ref cells[(int)blockMapCellIndex3];
				TItem val = newItems[l];
				int num2 = nativeArray2[(int)blockMapCellIndex3]++;
				itemsPackedByCell[reference2.itemStartIndex + num2] = val;
			}
			nativeArray2.Dispose();
		}
		catch
		{
			Reset();
			throw;
		}
		finally
		{
			nativeArray.Dispose();
		}
	}

	private Vector3Int WorldPositionToGridPosFloor(in Vector3 worldPosition)
	{
		Vector3 localPosition = worldPosition - worldBoundingBox.min;
		return LocalPositionToGridPosFloor(in localPosition);
	}

	private Vector3Int WorldPositionToGridPosCeil(in Vector3 worldPosition)
	{
		Vector3 localPosition = worldPosition - worldBoundingBox.min;
		return LocalPositionToGridPosCeil(in localPosition);
	}

	private Vector3Int LocalPositionToGridPosFloor(in Vector3 localPosition)
	{
		return Vector3Int.FloorToInt(Vector3.Scale(localPosition, invCellSize));
	}

	private Vector3Int LocalPositionToGridPosCeil(in Vector3 localPosition)
	{
		return Vector3Int.CeilToInt(Vector3.Scale(localPosition, invCellSize));
	}

	[MethodImpl(MethodImplOptions.AggressiveInlining)]
	private BlockMapCellIndex GridPosToCellIndex(in Vector3Int gridPos)
	{
		return (BlockMapCellIndex)((gridPos.y * cellCounts.z + gridPos.z) * cellCounts.x + gridPos.x);
	}

	private Vector3Int CellIndexToGridPos(BlockMapCellIndex cellIndex)
	{
		int num = (int)cellIndex;
		Vector3Int zero = Vector3Int.zero;
		int num2 = cellCounts.x * cellCounts.z;
		zero.y = num / num2;
		num -= zero.y * num2;
		zero.z = num / cellCounts.x;
		num -= zero.z * cellCounts.x;
		zero.x = num;
		return zero;
	}

	private Bounds GridPosToBounds(in Vector3Int gridPos)
	{
		Vector3 vector = gridPos;
		vector.Scale(cellSize);
		vector += worldBoundingBox.min;
		Bounds result = default(Bounds);
		result.SetMinMax(vector, vector + cellSize);
		return result;
	}

	private bool GridPosIsValid(Vector3Int gridPos)
	{
		if (gridPos.x >= 0 && gridPos.y >= 0 && gridPos.z >= 0 && gridPos.x < cellCounts.x && gridPos.y < cellCounts.y)
		{
			return gridPos.z < cellCounts.z;
		}
		return false;
	}

	public void GetItemsInSphere(Vector3 point, float radius, List<TItem> dest)
	{
		Bounds bounds = GetSphereBounds(point, radius);
		int count = dest.Count;
		GetBoundOverlappingCellItems(in bounds, dest);
		float num = radius * radius;
		for (int num2 = dest.Count - 1; num2 >= count; num2--)
		{
			TItem item = dest[num2];
			if ((itemPositionGetter.GetPosition3(item) - point).sqrMagnitude > num)
			{
				dest.RemoveAt(num2);
			}
		}
	}

	public void GetItemsInBounds(in Bounds bounds, List<TItem> dest)
	{
		int count = dest.Count;
		GetBoundOverlappingCellItems(in bounds, dest);
		for (int num = dest.Count - 1; num >= count; num--)
		{
			TItem item = dest[num];
			Vector3 position = itemPositionGetter.GetPosition3(item);
			if (!worldBoundingBox.Contains(position))
			{
				dest.RemoveAt(num);
			}
		}
	}

	private void GetBoundOverlappingCellItems(in Bounds bounds, List<TItem> dest)
	{
		List<BlockMapCellIndex> list = CollectionPool<BlockMapCellIndex, List<BlockMapCellIndex>>.RentCollection();
		GetBoundOverlappingCells(bounds, list);
		foreach (BlockMapCellIndex item in list)
		{
			ref BlockMapCell reference = ref cells[(int)item];
			int i = reference.itemStartIndex;
			for (int num = reference.itemStartIndex + reference.itemCount; i < num; i++)
			{
				dest.Add(itemsPackedByCell[i]);
			}
		}
		list = CollectionPool<BlockMapCellIndex, List<BlockMapCellIndex>>.ReturnCollection(list);
	}

	private void GetBoundOverlappingCells(Bounds bounds, List<BlockMapCellIndex> dest)
	{
		Vector3 worldPosition = bounds.min;
		Vector3Int vector3Int = WorldPositionToGridPosFloor(in worldPosition);
		worldPosition = bounds.max;
		Vector3Int vector3Int2 = WorldPositionToGridPosCeil(in worldPosition);
		vector3Int.Clamp(Vector3Int.zero, cellCounts);
		vector3Int2.Clamp(Vector3Int.zero, cellCounts);
		Vector3Int gridPos = vector3Int;
		gridPos.y = vector3Int.y;
		while (gridPos.y < vector3Int2.y)
		{
			gridPos.z = vector3Int.z;
			int x;
			while (gridPos.z < vector3Int2.z)
			{
				gridPos.x = vector3Int.x;
				while (gridPos.x < vector3Int2.x)
				{
					dest.Add(GridPosToCellIndex(in gridPos));
					x = gridPos.x + 1;
					gridPos.x = x;
				}
				x = gridPos.z + 1;
				gridPos.z = x;
			}
			x = gridPos.y + 1;
			gridPos.y = x;
		}
	}

	public bool GetNearestItemWhichPassesFilter<TItemFilter>(Vector3 position, float maxDistance, ref TItemFilter filter, out TItem dest) where TItemFilter : IBlockMapSearchFilter<TItem>
	{
		SingleSearchResultHandler searchResultHandler = default(SingleSearchResultHandler);
		GetNearestItemsWhichPassFilter(position, maxDistance, ref filter, ref searchResultHandler);
		dest = (searchResultHandler.foundResult ? searchResultHandler.result : default(TItem));
		return searchResultHandler.foundResult;
	}

	public void GetNearestItemsWhichPassFilter<TItemFilter>(Vector3 position, float maxDistance, ref TItemFilter filter, List<TItem> dest) where TItemFilter : IBlockMapSearchFilter<TItem>
	{
		ListWriteSearchResultHandler searchResultHandler = new ListWriteSearchResultHandler(dest);
		GetNearestItemsWhichPassFilter(position, maxDistance, ref filter, ref searchResultHandler);
	}

	private void GetNearestItemsWhichPassFilter<TItemFilter, TSearchResultHandler>(Vector3 position, float maxDistance, ref TItemFilter filter, ref TSearchResultHandler searchResultHandler) where TItemFilter : IBlockMapSearchFilter<TItem> where TSearchResultHandler : ISearchResultHandler
	{
		maxDistance = Mathf.Max(maxDistance, 0f);
		List<ItemDistanceSqrPair> candidatesInsideRadius = CollectionPool<ItemDistanceSqrPair, List<ItemDistanceSqrPair>>.RentCollection();
		List<ItemDistanceSqrPair> candidatesOutsideRadius = CollectionPool<ItemDistanceSqrPair, List<ItemDistanceSqrPair>>.RentCollection();
		float radiusSqr;
		try
		{
			Vector3Int gridPos = WorldPositionToGridPosFloor(in position);
			Bounds bounds = GridPosToBounds(in gridPos);
			Bounds currentBounds = bounds;
			float additionalRadius2 = cellSize.ComponentMin();
			float radius = DistanceToNearestWall(in position, in currentBounds);
			radiusSqr = radius * radius;
			BlockMapCellIndex cellIndex2 = GridPosToCellIndex(in gridPos);
			BoundsInt visitedCells = new BoundsInt(gridPos, Vector3Int.one);
			int visitedCellCount = 0;
			bool num = GridPosIsValid(gridPos);
			if (num)
			{
				VisitCell(cellIndex2);
			}
			if (!num)
			{
				AddRadius(Mathf.Sqrt(worldBoundingBox.SqrDistance(position)));
			}
			bool flag = true;
			while (flag)
			{
				while (candidatesInsideRadius.Count > 0)
				{
					int num2 = -1;
					float num3 = float.PositiveInfinity;
					for (int i = 0; i < candidatesInsideRadius.Count; i++)
					{
						ItemDistanceSqrPair itemDistanceSqrPair = candidatesInsideRadius[i];
						if (itemDistanceSqrPair.distanceSqr < num3)
						{
							num3 = itemDistanceSqrPair.distanceSqr;
							num2 = i;
						}
					}
					if (num2 != -1)
					{
						ItemDistanceSqrPair itemDistanceSqrPair2 = candidatesInsideRadius[num2];
						candidatesInsideRadius.RemoveAt(num2);
						bool shouldFinish = false;
						if ((filter.CheckItem(itemsPackedByCell[itemDistanceSqrPair2.itemIndex], ref shouldFinish) && !searchResultHandler.OnEncounterResult(itemsPackedByCell[itemDistanceSqrPair2.itemIndex])) || shouldFinish)
						{
							return;
						}
					}
				}
				flag = AddRadius(additionalRadius2);
			}
			bool AddRadius(float additionalRadius)
			{
				radius += additionalRadius;
				bool flag2 = radius >= maxDistance;
				if (flag2)
				{
					radius = maxDistance;
				}
				radiusSqr = radius * radius;
				currentBounds = GetSphereBounds(position, radius);
				for (int num4 = candidatesOutsideRadius.Count - 1; num4 >= 0; num4--)
				{
					ItemDistanceSqrPair item = candidatesOutsideRadius[num4];
					if (item.distanceSqr < radiusSqr)
					{
						candidatesOutsideRadius.RemoveAt(num4);
						candidatesInsideRadius.Add(item);
					}
				}
				bool flag3 = visitedCellCount >= cellCount1D;
				if (!flag3)
				{
					BoundsInt outer = WorldBoundsToOverlappingGridBoundsClamped(currentBounds);
					BoundsIntDifferenceEnumerable.BoundsIntDifferenceEnumerator enumerator = new BoundsIntDifferenceEnumerable(in outer, in visitedCells).GetEnumerator();
					while (enumerator.MoveNext())
					{
						Vector3Int gridPos2 = enumerator.Current;
						VisitCell(GridPosToCellIndex(in gridPos2));
					}
					visitedCells = outer;
				}
				if (candidatesInsideRadius.Count == 0)
				{
					if (flag2)
					{
						return false;
					}
					if (flag3 && candidatesOutsideRadius.Count == 0)
					{
						return false;
					}
				}
				return true;
			}
			void VisitCell(BlockMapCellIndex cellIndex)
			{
				int num5 = visitedCellCount + 1;
				visitedCellCount = num5;
				ref BlockMapCell reference = ref cells[(int)cellIndex];
				int j = reference.itemStartIndex;
				for (int num6 = reference.itemStartIndex + reference.itemCount; j < num6; j++)
				{
					VisitItem(j);
				}
			}
		}
		finally
		{
			candidatesOutsideRadius = CollectionPool<ItemDistanceSqrPair, List<ItemDistanceSqrPair>>.ReturnCollection(candidatesOutsideRadius);
			candidatesInsideRadius = CollectionPool<ItemDistanceSqrPair, List<ItemDistanceSqrPair>>.ReturnCollection(candidatesInsideRadius);
		}
		void VisitItem(int itemIndex)
		{
			Vector3 position2 = itemPositionGetter.GetPosition3(itemsPackedByCell[itemIndex]);
			ItemDistanceSqrPair itemDistanceSqrPair3 = default(ItemDistanceSqrPair);
			itemDistanceSqrPair3.itemIndex = itemIndex;
			itemDistanceSqrPair3.distanceSqr = (position2 - position).sqrMagnitude;
			ItemDistanceSqrPair item2 = itemDistanceSqrPair3;
			((item2.distanceSqr < radiusSqr) ? candidatesInsideRadius : candidatesOutsideRadius).Add(item2);
		}
	}

	private float DistanceToNearestWall(in Vector3 position, in Bounds bounds)
	{
		Vector3 vector = position - bounds.min;
		Vector3 vector2 = bounds.max - position;
		float x = vector.x;
		x = ((x < vector.y) ? x : vector.y);
		x = ((x < vector.z) ? x : vector.z);
		x = ((x < vector2.x) ? x : vector2.x);
		x = ((x < vector2.y) ? x : vector2.y);
		return (x < vector2.z) ? x : vector2.z;
	}

	private float DistanceToNearestWall(in Bounds innerBounds, in Bounds outerBounds)
	{
		Vector3 vector = innerBounds.min - outerBounds.min;
		Vector3 vector2 = outerBounds.max - innerBounds.max;
		float x = vector.x;
		x = ((x < vector.y) ? x : vector.y);
		x = ((x < vector.z) ? x : vector.z);
		x = ((x < vector2.x) ? x : vector2.x);
		x = ((x < vector2.y) ? x : vector2.y);
		return (x < vector2.z) ? x : vector2.z;
	}

	private static Bounds GetSphereBounds(Vector3 origin, float radius)
	{
		float num = radius * 2f;
		return new Bounds(origin, new Vector3(num, num, num));
	}

	private BoundsInt WorldBoundsOverlappingToGridBounds(Bounds worldBounds)
	{
		Vector3 worldPosition = worldBounds.min;
		Vector3Int minPosition = WorldPositionToGridPosFloor(in worldPosition);
		worldPosition = worldBounds.max;
		Vector3Int maxPosition = WorldPositionToGridPosCeil(in worldPosition);
		BoundsInt result = default(BoundsInt);
		result.SetMinMax(minPosition, maxPosition);
		return result;
	}

	private BoundsInt WorldBoundsToOverlappingGridBoundsClamped(Bounds worldBounds)
	{
		Vector3 worldPosition = worldBounds.min;
		Vector3Int minPosition = WorldPositionToGridPosFloor(in worldPosition);
		worldPosition = worldBounds.max;
		Vector3Int maxPosition = WorldPositionToGridPosCeil(in worldPosition);
		minPosition.Clamp(Vector3Int.zero, cellCounts);
		maxPosition.Clamp(Vector3Int.zero, cellCounts);
		BoundsInt result = default(BoundsInt);
		result.SetMinMax(minPosition, maxPosition);
		return result;
	}

	private GridEnumerable ValidGridPositionsInBounds(Bounds bounds)
	{
		Vector3 worldPosition = bounds.min;
		Vector3Int startPos = WorldPositionToGridPosFloor(in worldPosition);
		worldPosition = bounds.max;
		Vector3Int endPos = WorldPositionToGridPosCeil(in worldPosition);
		startPos.Clamp(Vector3Int.zero, cellCounts);
		endPos.Clamp(Vector3Int.zero, cellCounts);
		return new GridEnumerable(startPos, endPos);
	}

	private GridEnumerable ValidGridPositionsInBoundsWithExclusion(Bounds bounds, BoundsInt excludedCells)
	{
		Vector3 worldPosition = bounds.min;
		Vector3Int startPos = WorldPositionToGridPosFloor(in worldPosition);
		worldPosition = bounds.max;
		Vector3Int endPos = WorldPositionToGridPosCeil(in worldPosition);
		startPos.Clamp(Vector3Int.zero, cellCounts - Vector3Int.one);
		endPos.Clamp(Vector3Int.zero, cellCounts - Vector3Int.one);
		return new GridEnumerable(startPos, endPos);
	}
}
