using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SBRL.Algorithms
{
	/// <summary>
	/// Picks random items from a list, according to weights assigned to them.
	/// </summary>
	/// <remarks>
	/// Higher weights mean that an item is picked more frequently than the other items.
	/// </remarks>
	/// <license>Mozilla Public License version 2.0</license>
	/// <origin>https://gist.github.com/sbrl/9090a8c646b8d34b6e0170ddfd197d09</origin>
	/// <author>Starbeamrainbowlabs (https://starbeamrainbowlabs.com/)</author>
	/// <changelog>
	/// v0.1 - 20th May 2017:
	/// 	 - Creation! :D
	/// v0.2 - 17th Februrary 2018:
	/// 	 - Add Count property
	/// 	 - Add SetContents and ClearContents methods
	/// 	 - Add empty constructor
	/// 	 - Next() will now throw an InvalidOperationException if the generator's internal weights list is empty
	/// </changelog>
	public class WeightedRandom<ItemType>
	{
		private Random rand = new Random();

		protected ConcurrentDictionary<ItemType, double> weights = new ConcurrentDictionary<ItemType, double>();

		public int Count {
			get {
				return weights.Count;
			}
		}

		/// <summary>
		/// Creates a new weighted random number generator.
		/// </summary>
		/// <param name="items">The dictionary of weights and their corresponding items.</param>
		public WeightedRandom(IDictionary<ItemType, double> items)
		{
			SetContents(items);
		}
		/// <summary>
		/// Createse a new empty weighted random number generator.
		/// Remember to populate it before using!
		/// </summary>
		public WeightedRandom()
		{
		}

		public void SetContents(IDictionary<ItemType, double> items)
		{
			if (items.Count == 0)
				throw new ArgumentException("Error: The items dictionary provided is empty!");

			double totalWeight = items.Values.Sum();
			Parallel.ForEach(items, (KeyValuePair<ItemType, double> itemData) => {
				if (!weights.TryAdd(itemData.Key, itemData.Value / totalWeight))
					throw new Exception("WeightedRandom: Failed to add new weight definition to weights ConcurrentDictionary!");
			});
		}
		public void ClearContents()
		{
			weights.Clear();
		}

		/// <summary>
		/// Picks a new random item from the list provided at initialisation, based
		/// on the weights assigned to them.
		/// </summary>
		/// <returns>A random item, picked according to the assigned weights.</returns>
		public ItemType Next()
		{
			if (weights.Count == 0)
				throw new InvalidOperationException("Error: No weights specified! Add some with SetContents() before generating random numbers.");
			double target = rand.NextDouble();

			double lower = 0;
			double higher = 0;
			foreach (KeyValuePair<ItemType, double> weightData in weights)
			{
				higher += weightData.Value;
				if (target >= lower && target <= higher)
					return weightData.Key;
				lower += weightData.Value;
			}

			throw new Exception($"Error: Unable to find the weight that matches {target}");
		}
	}
}
