// Copyright Steve Streeting 2020 onwards
// Released under the MIT license

#pragma once

#include "CoreMinimal.h"
#include "Math/RandomStream.h"
#include "StevesShuffleBag.generated.h"

/// A shufflebag is an array that contains a certain number of items, and can randomly withdraw one
/// at a time until there are none left. No repeat items will be retrieved until the bag is empty.
/// Requires a type T which is the contained item (keep it small, will be copied around), and optionally
/// TRandStream which must be a type which implements the same interface as FRandomStream but may generate
/// numbers differently.
template<typename T, typename TRandStream = FRandomStream>
struct FStevesShuffleBag
{
protected:
	TArray<T> Bag;
	TRandStream RandomStream;
	// This is the dividing line between items still in the bag and those which have already been pulled.
	// Any index < SentinelIndex is an item in the bag, any >= SentinelIndex have already been pulled.
	int SentinelIndex;
	

public:
	/// Construct using a zero seed & standard capacity
	FStevesShuffleBag()
	{
		Init(0);
	}
	
	/// Construct using a known seed & capacity
	FStevesShuffleBag(int32 Seed, int Capacity = 32)
	{
		Init(Seed);
	}


	/// (Re-)initialise the shuffle bag, emptying its contents and resetting the seed.
	void Init(int32 Seed, int Capacity = 32)
	{
		Bag.Reset(Capacity);
		RandomStream.Initialize(Seed);
		SentinelIndex = -1;
	}
	
	/// Reserve space for a given number of entries
	void Reserve(int Capacity)
	{
		Bag.Reserve(Capacity);
	}
	
	/**
	 * Add one or more copies of an item to the bag.
	 * @param NewItem The new item to add
	 * @param Count The number of copies of this item to add. You can control the probability of items
	 * by adding multiple of the same thing.
	 * @note If you have already started pulling items from the bag, these items will NOT be available
	 * to be pulled until the bag is next re-filled. They will count as having been pulled already
	 * (so preferably add all your items before you start pulling items from the bag)
	 */
	void Add(const T& NewItem, int Count = 1)
	{
		for (int i = 0; i < Count; ++i)
		{
			Bag.Add(NewItem);
		}
	}

	/// Remove one instance of an item from the bag and discard it. This is different to pulling the
	/// item from the bag since it will not re-enter the bag on Reset. The item may have been pulled
	/// already.
	/// Returns whether any item was removed
	bool Remove(const T& Item)
	{
		int Index = Bag.IndexOfByKey(Item);
		if (Index != INDEX_NONE)
		{
			Bag.RemoveAt(Index);
			if (Index < SentinelIndex)
			{
				// This item hadn't been pulled yet
				--SentinelIndex;
			}
			return true;
		}

		return false;
	}

	/// Remove all instance of an item from the bag and discard them. This is different to pulling the
	/// items from the bag since they will not re-enter the bag on Reset.
	/// Returns the number of items removed.
	int RemoveAll(const T& Item)
	{
		int RemovedCount = 0;
		while (Remove(Item))
		{
			++RemovedCount;
		}
		return RemovedCount;
	}
	
	/// Empty the bag, discarding the contents
	void Empty()
	{
		// Keep allocations
		Bag.Reset();
		SentinelIndex = -1;
	}

	/// Reset the bag, returning all previously retrieved contents to the bag
	void Reset()
	{
		// Any items < SentinelIndex are in the bag
		SentinelIndex = Bag.Num();
	}

	/// Pull a random item from those remaining in the bag. If the bag is empty, it will be
	/// refilled using the tokens that were previously pulled (via Reset).
	T Next()
	{
		if (SentinelIndex <= 0)
		{
			// Empty / new bag
			Reset();
			
		}

		// Empty bag
		if (SentinelIndex == 0)
		{
			return T();
		}
		
		// rand max is inclusive, and we need to exclude >= SentinelIndex
		int NextIdx = RandomStream.RandRange(0, SentinelIndex - 1);
		// Copy out
		T Ret = Bag[NextIdx];
		
		--SentinelIndex;
		if (NextIdx != SentinelIndex) {
			// swap the item that's now beyond the sentinel into our slot
			Bag.Swap(NextIdx, SentinelIndex);
		}
		return Ret;
	}

	/// Get the number of items left in the bag
	int GetNumRemaining() const
	{
		return SentinelIndex > 0 ? SentinelIndex : 0;
	}

	/// Get the number of items which have already been pulled from the bag
	int GetNumPulled() const
	{
		return GetNumTotal() - GetNumRemaining();
	}

	/// Get the number of items both remaining in the bag and which have already been pulled from it
	int GetNumTotal() const
	{
		return Bag.Num();
	}

};

/// A shufflebag is an array that contains a certain number of items, and can randomly withdraw one
/// at a time until there are none left. No repeat items will be retrieved until the bag is empty.
/// Because Blueprints can't support templated types, FStevesIndexShuffleBag provides a concretely
/// typed shuffle bag which simply provides array indices rather than values. When you pull out an
/// item, instead of pulling out the *actual* item, you pull out an index into another array.
/// You can therefore use this with any other array so long as you keep them synchronised.
/// If you want to customise the probabilities, add multiple entries of the same item to *your* array
/// before constructing the index shuffle bag.
/// Use MakeIndexShuffleBag to create.
UCLASS(BlueprintType)
class UStevesIndexShuffleBag : public UObject
{
	GENERATED_BODY()

protected:
	FStevesShuffleBag<int> IndexBag;
public:

	UStevesIndexShuffleBag()
	{
	}
	
	/// Reinitialise the shuffle bag with a new size / seed.
	UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
	void Reinitialise(int Count, int32 Seed)
	{
		IndexBag.Init(Seed, Count);

		for (int i = 0; i < Count; ++i)
		{
			IndexBag.Add(i);
		}
	}

	
	/**
	 * Create a new shuffle bag of indexes for accessing items in an array
	 * @param Outer The 
	 * @param Count The number of items in the array you'll be accessing with these indexes
	 * @param Seed The seed for the random number generator
	 * @return The new index shuffle bag
	 */
	UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
	static UStevesIndexShuffleBag* MakeIndexShuffleBag(UObject* Outer, int Count, int32 Seed = 0)
	{
		auto Ret = NewObject<UStevesIndexShuffleBag>(Outer);
		Ret->Reinitialise(Count, Seed);
		return Ret;
	}

	

	/// Get the number of items left in the bag
	UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
	int GetNumRemaining() const
	{
		return IndexBag.GetNumRemaining();
	}

	/// Get the number of items which have already been pulled from the bag
	UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
	int GetNumPulled() const
	{
		return IndexBag.GetNumPulled();
	}

	/// Get the number of items both remaining in the bag and which have already been pulled from it
	UFUNCTION(BlueprintCallable, BlueprintPure, Category="Shuffle Bag")
	int GetNumTotal() const
	{
		return IndexBag.GetNumTotal();
	}

	/// Reset the bag, returning all previously retrieved contents to the bag
	UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
	void Reset()
	{
		IndexBag.Reset();
	}

	/// Pull a random index from those remaining in the bag. If the bag is empty, it will be
	/// refilled using the indexes that were previously pulled (via Reset).
	UFUNCTION(BlueprintCallable, Category="Shuffle Bag")
	int Next()
	{
		return IndexBag.Next();
	}
	
	
};

