
// userStatistics provides functions to generate statistics about stored user profiles.
// The user chooses the attribute(s), and the gui displays a chart with a button to view the statistics data.
// See statisticsGui.go to see the gui code.

package userStatistics

//TODO: Add the ability to control for confounding variables
// Example: Wealth, controlled for age and sex

import "seekia/internal/badgerDatabase"
import "seekia/internal/helpers"
import "seekia/internal/profiles/calculatedAttributes"
import "seekia/internal/profiles/profileStorage"
import "seekia/internal/profiles/attributeDisplay"
import "seekia/internal/translation"
import "seekia/internal/statisticsDatum"

import "slices"
import "strings"
import "errors"
import "math"


//Outputs:
//	-int: Number of users analyzed in statistics
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list (sorted, not grouped)
//	-bool: Grouping performed
//	-[]statisticsDatum.DiscreteStatisticsDatum: Grouped datums list
//	-func(float64)(string, error): Function to format y axis values
//		-This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable
//		-Example: "1000000" -> "1 million"
//	-error
func GetUserStatisticsDatumsLists_BarChart(identityType string,
										networkType byte,
										xAxisAttribute string,
										xAxisIsNumerical bool,
										formatXAxisValuesFunction func(string)(string, error),
										xAxisUnknownLabel string,
										yAxisAttribute string)(int, []statisticsDatum.DiscreteStatisticsDatum, bool, []statisticsDatum.DiscreteStatisticsDatum, func(float64)(string, error), error){

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return 0, nil, false, nil, nil, errors.New("GetUserStatisticsDatumsLists_BarChart called with invalid networkType: " + networkTypeString)
	}

	totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownDatum, unknownValueDatum, err := getBarAndLineChartsStatisticsDatumsList(networkType, identityType, xAxisAttribute, formatXAxisValuesFunction, xAxisUnknownLabel, yAxisAttribute)
	if (err != nil) { return 0, nil, false, nil, nil, err }	

	sortDiscreteStatisticsDatumsList(statisticsDatumsList, xAxisIsNumerical)

	// We now see if we need to group the datums in the list together
	// We do this if there are more than 10 categories

	if (len(statisticsDatumsList) <= 10){

		// No grouping needed. We are done.

		if (includeUnknownDatum == true){
			statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
		}

		return totalAnalyzedUsers, statisticsDatumsList, false, nil, formatValuesFunction, nil
	}

	groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(10, statisticsDatumsList, xAxisIsNumerical, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction)
	if (err != nil) { return 0, nil, false, nil, nil, err }

	if (includeUnknownDatum == true){
		statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
		groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum)
	}

	return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, formatValuesFunction, nil	
}

//Outputs:
//	-int: Number of users analyzed in statistics
//	-[]statisticsDatum.NumericStatisticsDatum: Statistics datums list used to create the line chart
//	-[]statisticsDatum.DiscreteStatisticsDatum: The Statistics datums list used to display the raw data results (not grouped)
//	-bool: Grouping performed
//	-[]statisticsDatum.DiscreteStatisticsDatum: Grouped datums list
//	-func(float64)(string, error): Function to format y axis values
//		-This is used because the values must be passed to the chart code as pure floats, but they must be formatted after to be human readable
//		-Example: "1000000" -> "1 million"
//	-error
func GetUserStatisticsDatumsLists_LineChart(identityType string,
										networkType byte,
										xAxisAttribute string,
										formatXAxisValuesFunction func(string)(string, error),
										xAxisUnknownLabel string,
										yAxisAttribute string)(int, []statisticsDatum.NumericStatisticsDatum, []statisticsDatum.DiscreteStatisticsDatum, bool, []statisticsDatum.DiscreteStatisticsDatum, func(float64)(string, error), error){

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return 0, nil, nil, false, nil, nil, errors.New("GetUserStatisticsDatumsLists_LineChart called with invalid networkType: " + networkTypeString)
	}

	totalAnalyzedUsers, discreteStatisticsDatumsList, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction, includeUnknownDatum, unknownValueDatum, err := getBarAndLineChartsStatisticsDatumsList(networkType, identityType, xAxisAttribute, formatXAxisValuesFunction, xAxisUnknownLabel, yAxisAttribute)
	if (err != nil) { return 0, nil, nil, false, nil, nil, err }	

	sortDiscreteStatisticsDatumsList(discreteStatisticsDatumsList, true)

	// We convert the discrete statistics datums list to a numeric statistics datums list
	//TODO: Rewrite this entire package to make this more elegant and efficient
	// I've added Line charts to a package that wasn't designed for them from the beginning.

	numericStatisticsDatumsList := make([]statisticsDatum.NumericStatisticsDatum, 0)

	for _, statisticsDatumItem := range discreteStatisticsDatumsList{

		datumLabelString := statisticsDatumItem.Label
		datumValue := statisticsDatumItem.Value

		datumLabel, err := helpers.ConvertStringToFloat64(datumLabelString)
		if (err != nil) {
			return 0, nil, nil, false, nil, nil, errors.New("Invalid statistics datum: Datum Label is not float: " + datumLabelString)
		}

		newNumericStatisticsDatum := statisticsDatum.NumericStatisticsDatum{
			XAxisValue: datumLabel,
			YAxisValue: datumValue,
		}

		numericStatisticsDatumsList = append(numericStatisticsDatumsList, newNumericStatisticsDatum)
	}

	// We now see if we need to group the datums in the list together
	// We do this if there are more than 25 unique X-axis values

	if (len(discreteStatisticsDatumsList) <= 25){

		// No grouping needed. We are done.

		if (includeUnknownDatum == true){
			discreteStatisticsDatumsList = append(discreteStatisticsDatumsList, unknownValueDatum)
		}

		return totalAnalyzedUsers, numericStatisticsDatumsList, discreteStatisticsDatumsList, false, nil, formatValuesFunction, nil
	}

	groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(25, discreteStatisticsDatumsList, true, responseCountsMap, yAxisIsAverage, responseSumsMap, formatValuesFunction)
	if (err != nil) { return 0, nil, nil, false, nil, nil, err }

	if (includeUnknownDatum == true){
		discreteStatisticsDatumsList = append(discreteStatisticsDatumsList, unknownValueDatum)
		groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum)
	}

	return totalAnalyzedUsers, numericStatisticsDatumsList, discreteStatisticsDatumsList, true, groupedStatisticsDatumsList, formatValuesFunction, nil	
}

//Outputs:
//	-int: Number of users analyzed in statistics
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list (sorted, not grouped)
//	-bool: Grouping performed
//	-[]statisticsDatum.DiscreteStatisticsDatum: Grouped datums list
//	-error
func GetUserStatisticsDatumsLists_DonutChart(identityType string,
											networkType byte,
											attributeToAnalyze string,
											attributeIsNumerical bool,
											formatAttributeLabelsFunction func(string)(string, error),
											unknownLabelTranslated string)(int, []statisticsDatum.DiscreteStatisticsDatum, bool, []statisticsDatum.DiscreteStatisticsDatum, error){

	isValid := helpers.VerifyNetworkType(networkType)
	if (isValid == false){
		networkTypeString := helpers.ConvertByteToString(networkType)
		return 0, nil, false, nil, errors.New("GetUserStatisticsDatumsLists_DonutChart called with invalid networkType: " + networkTypeString)
	}

	totalAnalyzedUsers, statisticsDatumsList, attributeCountsMap, numberOfUnknownResponders, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, attributeToAnalyze, formatAttributeLabelsFunction)
	if (err != nil) { return 0, nil, false, nil, err }

	sortDiscreteStatisticsDatumsList(statisticsDatumsList, attributeIsNumerical)

	getUnknownValueDatum := func()statisticsDatum.DiscreteStatisticsDatum{

		numberOfUnknownRespondersString := helpers.ConvertIntToString(numberOfUnknownResponders)

		unknownValueDatum := statisticsDatum.DiscreteStatisticsDatum{
			Label: unknownLabelTranslated,
			LabelFormatted: unknownLabelTranslated,
			Value: float64(numberOfUnknownResponders),
			ValueFormatted: numberOfUnknownRespondersString,
		}

		return unknownValueDatum
	}

	if (len(statisticsDatumsList) <= 8){

		// No grouping needed.

		if (numberOfUnknownResponders != 0){

			unknownValueDatum := getUnknownValueDatum()

			statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
		}

		return totalAnalyzedUsers, statisticsDatumsList, false, nil, nil
	}

	formatValuesFunction := func(input float64)(string, error){

		// input will always be the number of users who responded with the value
		// Thus, input will always be an integer

		result := helpers.ConvertFloat64ToStringRounded(input, 0)

		return result, nil
	}

	groupedStatisticsDatumsList, err := getStatisticsDatumsListGrouped(8, statisticsDatumsList, attributeIsNumerical, attributeCountsMap, false, nil, formatValuesFunction)
	if (err != nil) { return 0, nil, false, nil, err }

	if (numberOfUnknownResponders != 0){

		unknownValueDatum := getUnknownValueDatum()

		statisticsDatumsList = append(statisticsDatumsList, unknownValueDatum)
		groupedStatisticsDatumsList = append(groupedStatisticsDatumsList, unknownValueDatum)
	}

	return totalAnalyzedUsers, statisticsDatumsList, true, groupedStatisticsDatumsList, nil
}

// We use this function for bar and line charts
//Outputs:
//	-int: Total analyzed users
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list
//	-map[string]int: Response Counts map (X axis attribute response -> Number of y axis responses)
//	-bool: yAxisIsAverage
//	-map[string]float64: Response Sums map (X axis attribute response -> all y axis responses summed) (If yAxisIsAverage == true)
//	-func(float64)(string, error): Function to format statistics values
//	-bool: Include a No Response/Unknown value datum
//		-This is only needed if at least 1 user did not respond to the X axis attribute on their profile.
//	-statisticsDatum.DiscreteStatisticsDatum: The unknown value datum.
//	-error
func getBarAndLineChartsStatisticsDatumsList(networkType byte, identityType string, xAxisAttribute string, formatXAxisValuesFunction func(string)(string, error), xAxisUnknownLabel string, yAxisAttribute string)(int, []statisticsDatum.DiscreteStatisticsDatum, map[string]int, bool, map[string]float64, func(float64)(string, error), bool, statisticsDatum.DiscreteStatisticsDatum, error){

	getYAxisRoundingPrecision := func()int{
		if (yAxisAttribute == "Number Of Users"){
			return 0
		}
		if (yAxisAttribute == "Average Height"){
			return 0
		}
		if (yAxisAttribute == "Average Age"){
			return 0
		}
		return 1
	}

	yAxisRoundingPrecision := getYAxisRoundingPrecision()

	//TODO: Add "Probability Of ..."
	// This will allow viewing of choice attribute probabilities
	// For example: Probability of being Male, probability of being Female, etc.
	// We need to add this for each canonical option

	if (yAxisAttribute == "Number Of Users"){

		totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, err := getProfileAttributeCountStatisticsDatumsList(identityType, networkType, xAxisAttribute, formatXAxisValuesFunction)
		if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

		formatValuesFunction := func(input float64)(string, error){

			// input will be an whole number representing the number of users

			result := helpers.ConvertFloat64ToStringRounded(input, 0)

			return result, nil
		}

		if (numberOfUnknownValueUsers == 0){
			return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, false, statisticsDatum.DiscreteStatisticsDatum{}, nil
		}

		unknownValueFormatted := helpers.ConvertIntToString(numberOfUnknownValueUsers)

		unknownDatum := statisticsDatum.DiscreteStatisticsDatum{
			Label: xAxisUnknownLabel,
			LabelFormatted: xAxisUnknownLabel,
			Value: float64(numberOfUnknownValueUsers),
			ValueFormatted: unknownValueFormatted,
		}

		return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, false, nil, formatValuesFunction, true, unknownDatum, nil
	}

	userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
	if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

	yAxisIsAverage := strings.HasPrefix(yAxisAttribute, "Average ")
	if (yAxisIsAverage == true){

		// This function will return the attribute name
		// We must convert the attribute title to the attribute name
		getAttributeToGetAverageFor := func()string{

			attributeTitle := strings.TrimPrefix(yAxisAttribute, "Average ")

			switch attributeTitle{

				case "Wealth":{
					return "WealthInGold"
				}
				case "23andMe Neanderthal Variants":{
					return "23andMe_NeanderthalVariants"
				}
				case "Body Fat":{
					return "BodyFat"
				}
				case "Body Muscle":{
					return "BodyMuscle"
				}
				case "Fruit Rating":{
					return "FruitRating"
				}
				case "Vegetables Rating":{
					return "VegetablesRating"
				}
				case "Nuts Rating":{
					return "NutsRating"
				}
				case "Grains Rating":{
					return "GrainsRating"
				}
				case "Dairy Rating":{
					return "DairyRating"
				}
				case "Seafood Rating":{
					return "SeafoodRating"
				}
				case "Beef Rating":{
					return "BeefRating"
				}
				case "Pork Rating":{
					return "PorkRating"
				}
				case "Poultry Rating":{
					return "PoultryRating"
				}
				case "Eggs Rating":{
					return "EggsRating"
				}
				case "Beans Rating":{
					return "BeansRating"
				}
				case "Alcohol Frequency":{
					return "AlcoholFrequency"
				}
				case "Tobacco Frequency":{
					return "TobaccoFrequency"
				}
				case "Cannabis Frequency":{
					return "CannabisFrequency"
				}
				case "Pets Rating":{
					return "PetsRating"
				}
				case "Dogs Rating":{
					return "DogsRating"
				}
				case "Cats Rating":{
					return "CatsRating"
				}
			}

			return attributeTitle
		}

		attributeToGetAverageFor := getAttributeToGetAverageFor()

		totalAnalyzedUsers := 0

		//Map structure: X-Axis attribute -> Number of users with this x-axis attribute who have a y-axis attribute response
		responseCountsMap := make(map[string]int)
		//Map structure: X-axis attribute response -> All Y-axis attribute responses summed for users with this x-axis attribute
		responseSumsMap := make(map[string]float64)

		// This stores a count of the number of users whose X axis value is unknown
		// For example, if X axis is Age, this is the number of users who did not respond
		numberOfUsersWithUnknownXAxisValue := 0
		// This stores the total sum of all y axis values for users who have an unknown X axis value
		// For example, if the X axis attribute is age, and the Y axis attribute is average height,
		//		this stores the sum of heights of all users who have no age on their profile.
		usersWithUnknownXAxisValueYAxisValuesSum := float64(0)

		for _, userIdentityHash := range userIdentityHashesToAnalyzeList{

			profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }
			if (profileFound == false){
				continue
			}

			userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }
			if (userIsDisabled == true){
				continue
			}

			attributeExists, _, userAttributeToGetAverageForValue, err := getAnyUserAttributeValueFunction(attributeToGetAverageFor)
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }
			if (attributeExists == false){
				// This user did not respond to the attribute we are getting the average for
				// We will not add them to the statistics maps
				continue
			}

			totalAnalyzedUsers += 1

			userAttributeToGetAverageForValueFloat64, err := helpers.ConvertStringToFloat64(userAttributeToGetAverageForValue)
			if (err != nil) {
				return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, errors.New("Database corrupt: Contains invalid " + userAttributeToGetAverageForValue + " value: " + userAttributeToGetAverageForValue)
			}

			attributeFound, _, userXAxisAttributeValue, err := getAnyUserAttributeValueFunction(xAxisAttribute)
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }
			if (attributeFound == false){
				// This user did not respond to the X axis attribute
				// We calculate the average for users who do not respond and put it in its own category

				numberOfUsersWithUnknownXAxisValue += 1
				usersWithUnknownXAxisValueYAxisValuesSum += userAttributeToGetAverageForValueFloat64
				continue
			}

			responseCountsMap[userXAxisAttributeValue] += 1

			responseSumsMap[userXAxisAttributeValue] += userAttributeToGetAverageForValueFloat64
		}

		_, _, formatYAxisValuesFunction, _, _, err := attributeDisplay.GetProfileAttributeDisplayInfo(attributeToGetAverageFor)
		if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

		statisticsDatumsList := make([]statisticsDatum.DiscreteStatisticsDatum, 0, len(responseCountsMap))

		for attributeResponse, responsesCount := range responseCountsMap{

			attributeResponseFormatted, err := formatXAxisValuesFunction(attributeResponse)
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

			allResponsesSum, exists := responseSumsMap[attributeResponse]
			if (exists == false){
				return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, errors.New("Response sums map missing attribute value")	
			}

			averageValue := allResponsesSum/float64(responsesCount)

			averageValueString := helpers.ConvertFloat64ToStringRounded(averageValue, yAxisRoundingPrecision)

			averageValueFormatted, err := formatYAxisValuesFunction(averageValueString)
			if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

			newStatisticsDatum := statisticsDatum.DiscreteStatisticsDatum{
				Label: attributeResponse,
				LabelFormatted: attributeResponseFormatted,
				Value: averageValue,
				ValueFormatted: averageValueFormatted,
			}

			statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum)
		}

		// We use this function to format values after grouping, if grouping is needed
		formatValuesFunction := func(input float64)(string, error){

			inputString := helpers.ConvertFloat64ToStringRounded(input, yAxisRoundingPrecision)

			valueFormatted, err := formatYAxisValuesFunction(inputString)
			if (err != nil) { return "", err }

			return valueFormatted, nil
		}

		if (numberOfUsersWithUnknownXAxisValue == 0){
			return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, false, statisticsDatum.DiscreteStatisticsDatum{}, nil
		}

		unknownResponsesAverage := usersWithUnknownXAxisValueYAxisValuesSum/float64(numberOfUsersWithUnknownXAxisValue)

		unknownResponsesValueFormatted, err := formatValuesFunction(unknownResponsesAverage)
		if (err != nil) { return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, err }

		// This datum represents the average value for the yAxisAttribute for users who did not respond.
		// For example, if the xAxisAttribute is Height, and the yAxisAttribute is AverageWealth, this value
		//		will represent the average wealth for users who did not provide Height on their profile.
		unknownStatisticsDatum := statisticsDatum.DiscreteStatisticsDatum{
			Label: xAxisUnknownLabel,
			LabelFormatted: xAxisUnknownLabel,
			Value: unknownResponsesAverage,
			ValueFormatted: unknownResponsesValueFormatted,
		}

		return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, true, responseSumsMap, formatValuesFunction, true, unknownStatisticsDatum, nil
	}

	return 0, nil, nil, false, nil, nil, false, statisticsDatum.DiscreteStatisticsDatum{}, errors.New("Invalid y-axis attribute: " + yAxisAttribute)
}


// This function will return a statistics datums list of the following format:
//	"Label": Attribute name (Example: "Male")
//	"Value": The number of users who responded with the attribute (in this example: "Male")
//
// All users of provided identityType who are not disabled will be analyzed
//	-int: Number of analyzed users
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list (not sorted or grouped)
//	-map[string]int: Response counts map (Response -> Number of responders)
//	-int: Number of No Response/Unknown value responders
//	-error
func getProfileAttributeCountStatisticsDatumsList(identityType string,
											networkType byte,
											attributeName string,
											formatLabelsFunction func(string)(string, error))(int, []statisticsDatum.DiscreteStatisticsDatum, map[string]int, int, error){

	userIdentityHashesToAnalyzeList, err := getUserIdentityHashesToAnalyzeList(identityType)
	if (err != nil) { return 0, nil, nil, 0, err }

	totalAnalyzedUsers := 0

	// Map structure: User Attribute response -> Number of users who responded with the response
	responseCountsMap := make(map[string]int)

	// This stores the number of users for whom we do not know their value
	numberOfUnknownValueUsers := 0

	for _, userIdentityHash := range userIdentityHashesToAnalyzeList{

		profileFound, getAnyUserAttributeValueFunction, err := getRetrieveAnyAttributeFromUserNewestProfileFunction(userIdentityHash, networkType)
		if (err != nil) { return 0, nil, nil, 0, err }
		if (profileFound == false){
			continue
		}
		userIsDisabled, _, _, err := getAnyUserAttributeValueFunction("Disabled")
		if (err != nil) { return 0, nil, nil, 0, err }
		if (userIsDisabled == true){
			continue
		}

		totalAnalyzedUsers += 1

		attributeFound, _, attributeValue, err := getAnyUserAttributeValueFunction(attributeName)
		if (err != nil) { return 0, nil, nil, 0, err }
		if (attributeFound == false){
			numberOfUnknownValueUsers += 1
			continue
		}

		responseCountsMap[attributeValue] += 1
	}

	statisticsDatumsList := make([]statisticsDatum.DiscreteStatisticsDatum, 0, len(responseCountsMap))

	for attributeResponse, numberOfUsers := range responseCountsMap{

		attributeResponseFormatted, err := formatLabelsFunction(attributeResponse)
		if (err != nil) { return 0, nil, nil, 0, err }

		attributeNumberOfUsersString := helpers.ConvertIntToString(numberOfUsers)

		newStatisticsDatum := statisticsDatum.DiscreteStatisticsDatum{
			Label: attributeResponse,
			LabelFormatted: attributeResponseFormatted,
			Value: float64(numberOfUsers),
			ValueFormatted: attributeNumberOfUsersString,
		}

		statisticsDatumsList = append(statisticsDatumsList, newStatisticsDatum)
	}

	return totalAnalyzedUsers, statisticsDatumsList, responseCountsMap, numberOfUnknownValueUsers, nil
}

func sortDiscreteStatisticsDatumsList(inputStatisticsDatumsList []statisticsDatum.DiscreteStatisticsDatum, labelIsNumerical bool){

	if (len(inputStatisticsDatumsList) <= 1){
		return
	}

	if (labelIsNumerical == true){

		// We sort the datums by label values in ascending order
		// Example: Bar chart columns are ages, in order of youngest to oldest

		compareDatumsFunction := func(datum1 statisticsDatum.DiscreteStatisticsDatum, datum2 statisticsDatum.DiscreteStatisticsDatum)int{

			datum1Label := datum1.Label
			datum2Label := datum2.Label

			datum1Float64, err := helpers.ConvertStringToFloat64(datum1Label)
			if (err != nil) {
				panic("Invalid statistics datum: Datum Label is not float: " + datum1Label)
			}

			datum2Float64, err := helpers.ConvertStringToFloat64(datum2Label)
			if (err != nil) {
				panic("Invalid statistics datum: Datum Label is not float: " + datum2Label)
			}

			if (datum1Float64 < datum2Float64){
				return -1
			}

			if (datum1Float64 == datum2Float64){
				return 0
			}

			return 1
		}

		slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction)

		return
	}

	// We sort the datums by their values in descending order

	compareDatumsFunction := func(datum1 statisticsDatum.DiscreteStatisticsDatum, datum2 statisticsDatum.DiscreteStatisticsDatum)int{

		datum1Value := datum1.Value
		datum2Value := datum2.Value

		if (datum1Value == datum2Value){
			return 0
		}

		if (datum1Value < datum2Value){
			return 1
		}

		return -1
	}

	slices.SortFunc(inputStatisticsDatumsList, compareDatumsFunction)
}

// This function will group a statistics datums list.
// It will group Labels and their values to fit into a specified number of groups
// Example: "1","2","3","4" -> "1-2", "3-4"
//Inputs:
//	-int: Maximum groups to create
//	-[]statisticsDatum.DiscreteStatisticsDatum: Statistics datums list to group
//	-bool: Label is numerical
//		-If it is, we will group labels into groups of numbers.
//		-Otherwise, we will group all categories after the first maximumGroupsToCreate into a group called Other
//	-map[string]int
//		-Response Counts map (Attribute response -> Number of responders with that response)
//	-bool: Value is average
//		-If value is average, we will combine the values of each group and find their average
//		-Otherwise, we will find their sum
//	-map[string]float64
//		-Response Sums map (X-axis attribute response -> all Y-axis responses summed) (If yAxisIsAverage == true)
//	-func(float64)(string, error): This is the function we use to format the values
//Outputs:
//	-[]statisticsDatum.DiscreteStatisticsDatum: Grouped statistics datums list
//	-error
func getStatisticsDatumsListGrouped(maximumGroupsToCreate int,
								inputStatisticsDatumsList []statisticsDatum.DiscreteStatisticsDatum,
								labelIsNumerical bool,
								responseCountsMap map[string]int,
								valueIsAverage bool,
								responseSumsMap map[string]float64,
								formatValuesFunction func(float64)(string, error))([]statisticsDatum.DiscreteStatisticsDatum, error){

	if (len(inputStatisticsDatumsList) <= maximumGroupsToCreate){
		return nil, errors.New("maximumGroupsToCreate is <= length of input statistics datums list")
	}

	// We deep copy the statistics datums list to retain the sorted version
	// We need to retain both versions because the user can view the raw or grouped data in the GUI

	statisticsDatumsList := slices.Clone(inputStatisticsDatumsList)

	// We use this function to get the new value for a group of labels
	getGroupValue := func(datumsToCombineList []statisticsDatum.DiscreteStatisticsDatum)(float64, error){

		// This will count the total number of users who responded with the responses within this group
		// Example: Labels are "Blue", "Green", this variable will store the number of users who responded with either Blue or Green
		totalRespondersCount := float64(0)

		// This will store the sum for all response values within the group
		// We only need to add to this sum if valueIsAverage == true
		allReponsesSummed := float64(0)

		for _, statisticsDatum := range datumsToCombineList{

			datumLabel := statisticsDatum.Label

			responderCount, exists := responseCountsMap[datumLabel]
			if (exists == false){
				return 0, errors.New("responseCountsMap missing label: " + datumLabel)
			}

			totalRespondersCount += float64(responderCount)

			if (valueIsAverage == true){

				yAxisAttributeResponsesSum, exists := responseSumsMap[datumLabel]
				if (exists == false){
					return 0, errors.New("responseSumsMap missing label: " + datumLabel)
				}
				allReponsesSummed += yAxisAttributeResponsesSum
			}
		}

		if (valueIsAverage == false){

			return totalRespondersCount, nil
		}

		// The value is an average
		// We need to find the average for all of the user responses for the labels in the input list
		// The Values in the inputStatisticsDatumsList are averages
		// We can't average out the averages, because that will not give us the true average
		// We have to use the original sums for all group datums and average them

		if (totalRespondersCount == 0){
			return 0, errors.New("totalRespondersCount is 0.")
		}

		value := allReponsesSummed/float64(totalRespondersCount)

		return value, nil
	}

	if (labelIsNumerical == true){

		maximumDatumsPerCategory := int(math.Ceil(float64(len(statisticsDatumsList))/float64(maximumGroupsToCreate)))

		statisticsDatumsListSublists, err := helpers.SplitListIntoSublists(statisticsDatumsList, maximumDatumsPerCategory)
		if (err != nil) { return nil, err }

		groupedDatumsList := make([]statisticsDatum.DiscreteStatisticsDatum, 0, len(statisticsDatumsListSublists))

		for _, groupDatumsListSublist := range statisticsDatumsListSublists{

			if (len(groupDatumsListSublist) == 1){
				// Sometimes, a group with 1 datum will be created
				// This happens if the groups cannot be evenly divided, so there is a remainder of 1.
				// Example: 10->4 groups = 3, 3, 3, 1.
				//TODO: Prevent this from happening so groups always have more than 1 subdatum

				groupDatum := groupDatumsListSublist[0]

				groupedDatumsList = append(groupedDatumsList, groupDatum)
				continue
			}

			finalIndex := len(groupDatumsListSublist)-1

			initialDatum := groupDatumsListSublist[0]
			finalDatum := groupDatumsListSublist[finalIndex]

			initialLabel := initialDatum.Label
			initialLabelFormatted := initialDatum.LabelFormatted

			finalLabel := finalDatum.Label
			finalLabelFormatted := finalDatum.LabelFormatted

			groupValue, err := getGroupValue(groupDatumsListSublist)
			if (err != nil) { return nil, err }

			groupValueFormatted, err := formatValuesFunction(groupValue)
			if (err != nil) { return nil, err }

			newGroupStatisticsDatum := statisticsDatum.DiscreteStatisticsDatum{
				Label: initialLabel + "-" + finalLabel,
				LabelFormatted: initialLabelFormatted + "-" + finalLabelFormatted,
				Value: groupValue,
				ValueFormatted: groupValueFormatted,
			}

			groupedDatumsList = append(groupedDatumsList, newGroupStatisticsDatum)
		}

		return groupedDatumsList, nil
	}

	// Label is not numerical
	// We combine all categories after the first maximumGroupsToCreate into a category called Other

	datumsToKeep := statisticsDatumsList[:maximumGroupsToCreate]

	datumsToCombine := statisticsDatumsList[maximumGroupsToCreate:]

	otherTranslated := translation.TranslateTextFromEnglishToMyLanguage("Other")

	otherGroupValue, err := getGroupValue(datumsToCombine)
	if (err != nil) { return nil, err }

	otherGroupValueFormatted, err := formatValuesFunction(otherGroupValue)
	if (err != nil) { return nil, err }

	otherGroupDatum := statisticsDatum.DiscreteStatisticsDatum{
		Label: "Other",
		LabelFormatted: otherTranslated,
		Value: otherGroupValue,
		ValueFormatted: otherGroupValueFormatted,
	}

	groupedStatisticsDatumsList := append(datumsToKeep, otherGroupDatum)

	return groupedStatisticsDatumsList, nil
}


func getUserIdentityHashesToAnalyzeList(identityType string)([][16]byte, error){

	allUserIdentityHashesList, err := badgerDatabase.GetAllProfileIdentityHashes(identityType)
	if (err != nil) { return nil, err }

	if (len(allUserIdentityHashesList) < 10000){
		return allUserIdentityHashesList, nil
	}

	helpers.RandomizeListOrder(allUserIdentityHashesList)

	identityHashesToAnalyzeList := allUserIdentityHashesList[:10000]

	return identityHashesToAnalyzeList, nil
}

//Outputs:
//	-bool: Profile Exists
//	-func(string)(bool, int, string, error)
//	-error
func getRetrieveAnyAttributeFromUserNewestProfileFunction(identityHash [16]byte, networkType byte)(bool, func(string)(bool, int, string, error), error){

	newestProfileExists, profileVersion, _, _, _, newestProfileMap, err := profileStorage.GetNewestUserProfile(identityHash, networkType)
	if (err != nil) { return false, nil, err }
	if (newestProfileExists == false){
		return false, nil, nil
	}

	getAnyAttributeFromUserNewestProfileFunction, err := calculatedAttributes.GetRetrieveAnyProfileAttributeIncludingCalculatedFunction(profileVersion, newestProfileMap)
	if (err != nil) { return false, nil, err }

	return true, getAnyAttributeFromUserNewestProfileFunction, nil
}

