//due to the use of doubles, sometimes small rounding errors occur and a final prize selection row
 //that should have a prob lower bound of 0 will have one + or minus 1 x 10-10 or smaller.
 //we will replace that with true 0
 private void AdjustFinalPrizeSelectionRow(PrizeSelectionRow prizeSelectionRow)
 {
     if (Math.Abs(prizeSelectionRow.PrizeProbabilityLowerBound) < 0.0000000001)
     {
         prizeSelectionRow.PrizeProbabilityLowerBound = 0;
     }
 }
        //lower indexes are "higher up" in the table, and require larger numbers to hit
        //the value part of the dictionary (the double) represents the lower bound of the random number needed to hit this relic
        public IList <PrizeSelectionRow> GetPrizeSelectionTable(IList <PrizeCategorySpecification> prizeCategorySpecifications)
        {
            #region validations
            if (prizeCategorySpecifications == null || !prizeCategorySpecifications.Any())
            {
                throw new ArgumentException("prizeCategorySpecifications must be greater non null and have 1 or more members");
            }
            if (prizeCategorySpecifications.Any(ps => ps.ProbabilityExtentForEntireCategory < 0.0) || prizeCategorySpecifications.Any(ps => ps.ProbabilityExtentForEntireCategory > 1.0))
            {
                throw new ArgumentException($"prizeCategorySpecifications must be between 0 and 1");
            }
            if (prizeCategorySpecifications.Any(ps => ps.PrizeCount <= 0))
            {
                throw new ArgumentException($"PrizeCount must be between greater than 0");
            }
            #endregion

            int    currentMaxPrizeIndex         = 0;   //this is the point from which we start adding other probability keys to the bottom.
            double currentProbabilityLowerBound = 1.0; //this is the point from which we start adding other probability ranges to the bottom.

            IList <PrizeSelectionRow> prizeSelectionTable = new List <PrizeSelectionRow>();


            foreach (var prizeCategorySpecification in prizeCategorySpecifications)
            {
                double probabilityIncrement = prizeCategorySpecification.ProbabilityExtentForEntireCategory / prizeCategorySpecification.PrizeCount;
                for (int counter = 0; counter < prizeCategorySpecification.PrizeCount; counter++)
                {
                    //pullCategoryProbabilityTable.Add(relicIndex + initialMaxRelicKey + 1, initialLowerBound - (probabilityIncrement * (relicIndex + 1))); //we start from the top prize and work down the table
                    PrizeSelectionRow row = new PrizeSelectionRow()
                    {
                        PrizeIndex = counter + currentMaxPrizeIndex + 1,
                        PrizeProbabilityLowerBound = currentProbabilityLowerBound - (probabilityIncrement * (counter + 1)),
                        PrizeCategoryName          = prizeCategorySpecification.PrizeCategoryName,
                        PrizeName = prizeCategorySpecification.PrizeNames[counter]
                    };

                    prizeSelectionTable.Add(row);
                }

                currentProbabilityLowerBound = prizeSelectionTable.OrderBy(row => row.PrizeIndex).Last().PrizeProbabilityLowerBound;
                currentMaxPrizeIndex         = prizeSelectionTable.OrderBy(row => row.PrizeIndex).Last().PrizeIndex;
            }

            //correct last row for 0 lower bound if that is what is clearly intended:
            AdjustFinalPrizeSelectionRow(prizeSelectionTable[currentMaxPrizeIndex - 1]);

            return(prizeSelectionTable);
        }
        //null row = no matching prizes were found
        private PrizeSelectionRow SelectPrizeFromPrizeTable(IList <PrizeSelectionRow> prizeSelectionTable, double randomNumber)
        {
            PrizeSelectionRow selectedPrize = null;

            //scan table from the top down until we find the first prize whose lower bound is less than or equal to than the provided roll;
            //the prize we want is the one immediately lower in the table.

            //top down - assume already sorted!!
            foreach (PrizeSelectionRow prizeSelectionRow in prizeSelectionTable)
            {
                if (prizeSelectionRow.PrizeProbabilityLowerBound <= randomNumber)
                {
                    selectedPrize = prizeSelectionRow;
                    break;
                }
            }

            return(selectedPrize);
        }
        public IList <PrizeResultRow> SelectPrizes(IList <SelectionDomain> selectionDomains, Random random = null)
        {
            #region Validations
            foreach (var selectionDomain in selectionDomains)
            {
                if (selectionDomain.PrizesToSelectFromDomainCount <= 0)
                {
                    throw new ArgumentException($"PrizesToSelectFromDomainCount for SelectFromDomain {selectionDomain.SelectionDomainName} must be greater than 0");
                }
                if (!_prizeSelectionTableHelper.IsPrizeSelectionTableValid(selectionDomain.PrizeSelectionTable))
                {
                    throw new ArgumentException($"PrizeSelectionTable for selectionDomain {selectionDomain.SelectionDomainName} was invalid");
                }
            }
            #endregion

            Stopwatch sw = Stopwatch.StartNew();

            //Set up variables and structures

            if (random == null)
            {
                random = new Random();
            }

            IList <PrizeNameCategoryPair> uniquePrizeNames = GetUniquePrizeNameCategoryPairs(selectionDomains);

            int finalPrizeRowCount = uniquePrizeNames.Count();

            IList <PrizeResultRow> prizeResultTable = new List <PrizeResultRow>(finalPrizeRowCount);

            List <IDictionary <int, int> > resultSummariesList = new List <IDictionary <int, int> >();


            //perform the actual selection and counting
            for (int domainCounter = 0; domainCounter < selectionDomains.Count; domainCounter++)
            {
                SelectionDomain currentSelectionDomain = selectionDomains[domainCounter];

                //for intermediate use because we need to update the prize counts in a performant way.
                IDictionary <int, int> resultsSummary = _prizeResultsTableHelper.GetEmptyPrizeResultsSummary(currentSelectionDomain.PrizeSelectionTable.Count);

                for (int counter = 0; counter < currentSelectionDomain.PrizesToSelectFromDomainCount; counter++)
                {
                    PrizeSelectionRow selectedPrizeRow = SelectPrizeFromPrizeTable(currentSelectionDomain.PrizeSelectionTable, random.NextDouble());

                    if (selectedPrizeRow != null)
                    {
                        resultsSummary[selectedPrizeRow.PrizeIndex]++;
                    }
                }

                resultSummariesList.Add(resultsSummary);
            }

            //now that we have the counts of selected prizes by index, we need to translate them back to the more
            //informative structure of the prize results table

            //generate table with all the data except counts and indexes
            prizeResultTable = uniquePrizeNames.Select(psr =>
                                                       new PrizeResultRow()
            {
                PrizeCategoryName  = psr.CategoryName,
                PrizeIndex         = 0,
                PrizeName          = psr.PrizeName,
                PrizeSelectedCount = 0
            }).ToList();

            //put in indexes
            for (int i = 0; i < finalPrizeRowCount; i++)
            {
                prizeResultTable[i].PrizeIndex = i + 1;
            }



            //now insert the counts; assumption is that the keys of the generated resultsSummary dictionary must match in number
            //and start at the same index (1), as the largestSelectionDomain which is used to prepopulate the prizeResultTable
            for (int domainCounter = 0; domainCounter < selectionDomains.Count; domainCounter++)
            {
                //since the resultsSummary dictionaries don't have names embedded, we can only relate the counts
                //to prize name in context of the associated selection domain.
                SelectionDomain        currentSelectionDomain = selectionDomains[domainCounter];
                IDictionary <int, int> resultsSummary         = resultSummariesList[domainCounter];

                //walk through prizeResultTable one prize at a time, ADDING in counts

                //walk through non zero resultsSummary one prize at a time, linking to prize name and ADDING in counts
                for (int item = 0; item < resultsSummary.Count; item++)
                {
                    if (resultsSummary[item + 1] > 0)
                    {
                        //get prize name from selection domain prizeSelectionTable
                        string prizeName = currentSelectionDomain.PrizeSelectionTable
                                           .Where(r => r.PrizeIndex == (item + 1)).Select(r => r.PrizeName).Single();

                        //now map prizeName back to prizeResultTable
                        PrizeResultRow rowToUpdate = prizeResultTable.Single(p => p.PrizeName == prizeName);

                        rowToUpdate.PrizeSelectedCount += resultsSummary[item + 1];
                    }
                }
            }
            sw.Stop();
            _logger.LogDebug($"finished a selection operation in {sw.ElapsedMilliseconds} milliseconds");

            return(prizeResultTable);
        }