
// traits provides information about traits and the SNPs that influence them

package traits

import "errors"

type Trait struct{

	// Example: "Eye Color"
	TraitName string

	TraitDescription string

	// This describes if the trait is discrete or numeric
	// Discrete traits have a set of outcomes (Example: Eye Color: Blue, Green...)
	// Numeric traits have a numeric outcome (Example: Height)
	// The value of this variable is either "Discrete" or "Numeric"
	DiscreteOrNumeric string

	// This is a list of rsIDs which are known to have an effect on this trait
	// These loci may not have any associated rules
	// We use these loci to predict trait outcomes with neural networks.
	// We also use these loci to calculate Deep Appearance Similarity.
	// Map Structure: rsID -> (map[Reference Name]Reference Link)
	LocusReferencesMap map[int64]map[string]string

	// This is a list of all loci used to predict this trait
	// If a neural network exists, all of these will be used as input into the network for prediction
	LociList []int64

	// This is a list of all loci used to predict this trait using rules
	// It is sometimes a subset of LociList
	LociList_Rules []int64

	// This list can be empty if no rules exist
	// An empty list means we are relying on LociList and neural networks for trait prediction.
	RulesList []TraitRule

	// List of outcomes
	// Example: "Lactose Intolerant", "Lactore Tolerant"
	// This list can be empty if outcomes are not text descriptions (Example: Facial structure)
	// If the trait is Numeric, or their or no rules nor a neural network, then this list will be empty.
	OutcomesList []string

	// This function returns a formatted, translated representation of a numeric value for this trait
	// For example, "150 centimeters", "5 foot 10 inches", "4/10"
	// Inputs:
	//	-float64: The value to format
	//	-bool: Show "/10" units
	//		-The "/10" units are used for certain traits, where the unit value is not a real-world measurement (such as length, weight, etc.)
	//		-An example of an "/10" trait is Homosexualness, which is represented by a value between 0 and 10.
	//		-We don't want to "/10" if we are displaying a confidence range (Example: +/- 5.2)
	//		-If we did, then people would get confused by thinking the unit represents a fraction
	NumericValueFormatter func(float64, bool)(string, error)

	// This map contains scientific resources about this trait
	// Map structure: Reference name -> Reference link
	ReferencesMap map[string]string
}

type TraitRule struct{

	// 3 byte identifier encoded hex
	RuleIdentifier string 

	// A list of RuleLocus objects which comprise the rule
	// The genome must have a required base pair for each locus in this list to pass the rule
	LociList []RuleLocus

	// The outcome that this rule will effect
	// The number of points to add to the outcome if the rule RSID values are fulfilled
	// Do not use negative values

	// Map Structure: Outcome name -> Points to add to outcome if rule passes
	OutcomePointsMap map[string]int

	// Map structure: Reference name -> Reference link
	ReferencesMap map[string]string
}


type RuleLocus struct{

	// 3 byte hex encoded string
	LocusIdentifier string

	// RSID that represents this locus
	// If multiple RSIDs represent the same locus, use the first rsid for the locus in the locusMetadata package
	LocusRSID int64

	// List of base pair values that this RSID must fulfill to pass the rule
	// As long as the value matches any base pair value in the list, the genome has passed this rule locus
	// The genome must pass every rule locus within a rule to pass the rule
	BasePairsList []string
}


var traitNamesList []string
var traitObjectsList []Trait

// Map Structure: Rule locus identifier -> RSID representing this locus 
var locusRSIDsMap map[string]int64

func InitializeTraitVariables()error{

	lactoseToleranceObject := getLactoseToleranceTraitObject()
	hairTextureObject := getHairTextureTraitObject()
	facialStructureObject := getFacialStructureTraitObject()
	eyeColorObject := getEyeColorTraitObject()
	hairColorObject := getHairColorTraitObject()
	skinColorObject := getSkinColorTraitObject()
	homosexualnessObject := getHomosexualnessTraitObject()

	heightObject, err := getHeightTraitObject()
	if (err != nil){ return err }


	traitObjectsList = []Trait{lactoseToleranceObject, hairTextureObject, facialStructureObject, eyeColorObject, hairColorObject, skinColorObject, heightObject, homosexualnessObject}

	traitNamesList = make([]string, 0, len(traitObjectsList))
	locusRSIDsMap = make(map[string]int64)

	for _, traitObject := range traitObjectsList{

		traitName := traitObject.TraitName

		traitNamesList = append(traitNamesList, traitName)

		traitRulesList := traitObject.RulesList

		for _, traitRuleObject := range traitRulesList{

			ruleLociList := traitRuleObject.LociList

			for _, ruleLocusObject := range ruleLociList{

				locusIdentifier := ruleLocusObject.LocusIdentifier
				locusRSID := ruleLocusObject.LocusRSID

				locusRSIDsMap[locusIdentifier] = locusRSID
			}
		}
	}

	return nil
}

// Be aware that all of these functions are returning original objects/slices, not copies
// Thus, we cannot edit the objects/slices that are returned. We must copy the fields first if we want to edit them.

func GetTraitNamesList()([]string, error){

	if (traitNamesList == nil){
		return nil, errors.New("GetTraitNamesList called when list is not initialized.")
	}
	
	return traitNamesList, nil
}

func GetTraitObjectsList()([]Trait, error){

	if (traitObjectsList == nil){
		return nil, errors.New("GetTraitObjectsList called when list is not initialized.")
	}

	return traitObjectsList, nil
}

func GetTraitObject(traitName string)(Trait, error){

	traitObjectsList, err := GetTraitObjectsList()
	if (err != nil) { return Trait{}, err }

	for _, traitObject := range traitObjectsList{

		currentTraitName := traitObject.TraitName
		if (currentTraitName != traitName){
			continue
		}

		return traitObject, nil
	}

	return Trait{}, errors.New("GetTraitObject called with unknown trait: " + traitName)
}

//Outputs:
//	-map[string]TraitRule: Map of RuleIdentifier -> Rule Object
//	-error (will return err if traitName is not found)
func GetTraitRulesMap(traitName string)(map[string]TraitRule, error){

	traitObject, err := GetTraitObject(traitName)
	if (err != nil) { return nil, err }

	traitRulesList := traitObject.RulesList

	traitRulesMap := make(map[string]TraitRule)

	for _, ruleObject := range traitRulesList{

		ruleIdentifier := ruleObject.RuleIdentifier
		traitRulesMap[ruleIdentifier] = ruleObject
	}

	return traitRulesMap, nil
}

func GetTraitRuleObject(traitName string, ruleIdentifier string)(TraitRule, error){

	traitRulesMap, err := GetTraitRulesMap(traitName)
	if (err != nil){ return TraitRule{}, err }

	ruleObject, exists := traitRulesMap[ruleIdentifier]
	if (exists == false){
		return TraitRule{}, errors.New("GetTraitRuleObject called with unknown ruleIdentifier: " + ruleIdentifier)
	}

	return ruleObject, nil
}

//Outputs:
//	-int64: The rsID which represents this locus
//	-error
func GetTraitRuleLocusRSID(locusIdentifier string)(int64, error){

	if (locusRSIDsMap == nil){
		return 0, errors.New("GetTraitRuleLocusRSID called when locusRSIDsMap is not initialized.")
	}

	locusRSID, exists := locusRSIDsMap[locusIdentifier]
	if (exists == false){
		return 0, errors.New("GetTraitRuleLocusRSID called with unknown locusIdentifier: " + locusIdentifier)
	}

	return locusRSID, nil
}

