
// viewedContent provides functions to generate and retrieve the viewedContent list
// This list is used to view content on the View Content page

package viewedContent

import "seekia/internal/appMemory"
import "seekia/internal/badgerDatabase"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/moderation/contentControversy"
import "seekia/internal/moderation/reviewStorage"
import "seekia/internal/myDatastores/myList"
import "seekia/internal/myDatastores/myMap"
import "seekia/internal/mySettings"

import "slices"
import "sync"
import "errors"

//TODO: Add more sort by attributes
// Examples: Number of reviews, continuous approval period

// This mutex will be locked whenever we update the viewed content list
var updatingViewedContentMutex sync.Mutex

var viewedContentListDatastore *myList.MyList

var viewedContentFiltersMapDatastore *myMap.MyMap

// This function must be called whenever an app user signs in
func InitializeViewedContentDatastores()error{

	updatingViewedContentMutex.Lock()
	defer updatingViewedContentMutex.Unlock()

	newViewedContentListDatastore, err := myList.CreateNewList("ViewedContent")
	if (err != nil) { return err }

	newViewedContentFiltersMapDatastore, err := myMap.CreateNewMap("ViewedContentFilters")
	if (err != nil){ return err }

	viewedContentListDatastore = newViewedContentListDatastore

	viewedContentFiltersMapDatastore = newViewedContentFiltersMapDatastore

	return nil
}

func GetViewedContentSortByAttribute()(string, error){
	
	exists, currentAttribute, err := mySettings.GetSetting("ViewedContentSortByAttribute")
	if (err != nil) { return "", err }
	if (exists == false){
		return "Controversy", nil
	} 
	
	return currentAttribute, nil
}

func GetViewedContentSortDirection()(string, error){
	
	exists, sortDirection, err := mySettings.GetSetting("ViewedContentSortDirection")
	if (err != nil) { return "", err }
	if (exists == false){
		return "Descending", nil
	}
	if (sortDirection != "Ascending" && sortDirection != "Descending"){
		return "", errors.New("MySettings malformed: Contains invalid ViewedContentSortDirection: " + sortDirection)
	}

	return sortDirection, nil
}

func CheckIfViewedContentNeedsRefresh()(bool, error){

	exists, needsRefresh, err := mySettings.GetSetting("ViewedContentNeedsRefreshYesNo")
	if (err != nil) { return false, err }
	if (exists == true && needsRefresh == "No") {	
		return false, nil
	}

	return true, nil
}

func GetViewedContentIsReadyStatus(networkType byte)(bool, error){

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

	exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus")
	if (err != nil) { return false, err }
	if (exists == false || contentGeneratedStatus != "Yes"){
		return false, nil
	}

	exists, contentSortedStatus, err := mySettings.GetSetting("ViewedContentSortedStatus")
	if (err != nil) { return false, err }
	if (exists == false || contentSortedStatus != "Yes"){
		return false, nil
	}

	exists, contentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType")
	if (err != nil) { return false, err }
	if (exists == false){
		// This should not happen, because ViewedContentNetworkType is created whenever content is generated
		return false, errors.New("mySettings missing ViewedContentNetworkType when ViewedContentGeneratedStatus exists.")
	}

	contentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(contentNetworkTypeString)
	if (err != nil) {
		return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + contentNetworkTypeString)
	}
	if (contentNetworkType != networkType){
		// Content wes generated for a different networkType
		// This should never happen, because we will always set ViewedContentGeneratedStatus to No when we switch network types,
		//		and we will always call the GetViewedContentIsReadyStatus function with the current appNetworkType

		//TODO: Log this.

		err := mySettings.SetSetting("ViewedContentGeneratedStatus", "No")
		if (err != nil) { return false, err }

		return false, nil
	}

	return true, nil
}

//Outputs:
//	-bool: Viewed content map list is ready
//	-[]string
//	-error
func GetViewedContentList(networkType byte)(bool, []string, error){

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

	areReady, err := GetViewedContentIsReadyStatus(networkType)
	if (err != nil) { return false, nil, err }
	if (areReady == false){
		return false, nil, nil
	}

	viewedContentList, err := viewedContentListDatastore.GetList()
	if (err != nil) { return false, nil, err }

	return true, viewedContentList, nil
}

// This function returns the number of viewed contents
// It can be called before the list is ready (after being generated, but before being sorted)
func GetNumberOfGeneratedViewedContents()(int, error){

	currentViewedContentList, err := viewedContentListDatastore.GetList()
	if (err != nil) { return 0, err }

	lengthInt := len(currentViewedContentList)

	return lengthInt, nil
}

//Outputs:
//	-bool: Build encountered error
//	-string: Error encountered
//	-bool: Build is stopped (will be stopped if user went to different page)
//	-bool: Viewed content is ready
//	-float64: Percentage Progress (0 - 1)
//	-error
func GetViewedContentBuildStatus(networkType byte)(bool, string, bool, bool, float64, error){

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

	exists, encounteredError := appMemory.GetMemoryEntry("ViewedContentBuildEncounteredError")
	if (exists == false){
		// No build exists. A build has not been started since Seekia was started
		return false, "", false, false, 0, nil
	}
	if (encounteredError == "Yes"){
		exists, errorEncountered := appMemory.GetMemoryEntry("ViewedContentBuildError")
		if (exists == false){
			return false, "", false, false, 0, errors.New("Viewed Content build encountered error is yes, but no error exists.")
		}

		return true, errorEncountered, false, false, 0, nil
	}

	isStopped := CheckIfBuildViewedContentIsStopped()
	if (isStopped == true){
		return false, "", true, false, 0, nil
	}
	
	contentIsReadyBool, err := GetViewedContentIsReadyStatus(networkType)
	if (err != nil) { return false, "", false, false, 0, err }
	if (contentIsReadyBool == true){

		return false, "", false, true, 1, nil	
	}

	exists, currentPercentageString := appMemory.GetMemoryEntry("ViewedContentReadyProgressStatus")
	if (exists == false){
		// No build exists. A build has not been started since Seekia was started
		return false, "", false, false, 0, nil
	}

	currentPercentageFloat, err := helpers.ConvertStringToFloat64(currentPercentageString)
	if (err != nil){
		return false, "", false, false, 0, errors.New("ViewedContentReadyProgressStatus is invalid: Not a float: " + currentPercentageString)
	}

	return false, "", false, false, currentPercentageFloat, nil
}


func CheckIfBuildViewedContentIsStopped()bool{

	exists, buildStoppedStatus := appMemory.GetMemoryEntry("StopBuildViewedContentYesNo")
	if (exists == false || buildStoppedStatus != "No") {
		return true
	}

	return false
}


// This function will cancel the current build (if one is running)
// It will then start updating our viewed content
func StartUpdatingViewedContent(networkType byte)error{

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

	appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "Yes")

	// We wait for any existing build to stop
	updatingViewedContentMutex.Lock()

	appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "No")
	appMemory.SetMemoryEntry("ViewedContentBuildError", "")
	appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0")

	appMemory.SetMemoryEntry("StopBuildViewedContentYesNo", "No")

	updateViewedContent := func()error{

		getViewedContentNeedsToBeGeneratedStatus := func()(bool, error){

			exists, contentGeneratedStatus, err := mySettings.GetSetting("ViewedContentGeneratedStatus")
			if (err != nil) { return false, err }
			if (exists == false || contentGeneratedStatus != "Yes"){
				return true, nil
			}

			exists, viewedContentNetworkTypeString, err := mySettings.GetSetting("ViewedContentNetworkType")
			if (err != nil) { return false, err }
			if (exists == false){
				// This should not happen, because ViewedContentNetworkType is set to Yes whenever viewed content is generated
				return false, errors.New("ViewedContentNetworkType missing when ViewedContentGeneratedStatus exists.")
			}

			viewedContentNetworkType, err := helpers.ConvertNetworkTypeStringToByte(viewedContentNetworkTypeString)
			if (err != nil) {
				return false, errors.New("mySettings contains invalid ViewedContentNetworkType: " + viewedContentNetworkTypeString)
			}
			if (viewedContentNetworkType != networkType){
				// This should not happen, because ViewedContentGeneratedStatus should be set to No whenever app network type is changed,
				//		and StartUpdatingViewedContent should only be called with the current app network type.
				return true, nil
			}

			return false, nil
		}

		viewedContentNeedsToBeGenerated, err := getViewedContentNeedsToBeGeneratedStatus()
		if (err != nil) { return err }
		if (viewedContentNeedsToBeGenerated == true){

			err := mySettings.SetSetting("ViewedContentSortedStatus", "No")
			if (err != nil) { return err }

			//TODO: Check if content passes filters
			//TODO: Omit disabled profiles

			viewedContentList := make([]string, 0)

			reportedMessageHashesList, err := badgerDatabase.GetAllReportedMessageHashes()
			if (err != nil) { return err }

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.10")

			reviewedMessageHashesList, err := badgerDatabase.GetAllReviewedMessageHashes()
			if (err != nil) { return err }

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.15")

			allMessageHashesList := helpers.CombineTwoListsAndAvoidDuplicates(reportedMessageHashesList, reviewedMessageHashesList)

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.20")

			for _, messageHash := range allMessageHashesList{

				messageHashString := encoding.EncodeBytesToHexString(messageHash[:])

				viewedContentList = append(viewedContentList, messageHashString)
			}

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.25")

			mateProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Mate")
			if (err != nil) { return err }

			for _, profileHash := range mateProfileHashesList{

				profileHashString := encoding.EncodeBytesToHexString(profileHash[:])

				viewedContentList = append(viewedContentList, profileHashString)
			}

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.35")

			hostProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Host")
			if (err != nil) { return err }

			for _, profileHash := range hostProfileHashesList{

				profileHashString := encoding.EncodeBytesToHexString(profileHash[:])

				viewedContentList = append(viewedContentList, profileHashString)
			}

			appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.45")

			moderatorProfileHashesList, err := badgerDatabase.GetAllProfileHashes("Moderator")
			if (err != nil) { return err }

			for _, profileHash := range moderatorProfileHashesList{

				profileHashString := encoding.EncodeBytesToHexString(profileHash[:])

				viewedContentList = append(viewedContentList, profileHashString)
			}

			err = viewedContentListDatastore.OverwriteList(viewedContentList)
			if (err != nil) { return err }

			err = mySettings.SetSetting("ViewedContentGeneratedStatus", "Yes")
			if (err != nil) { return err }

			networkTypeString := helpers.ConvertByteToString(networkType)

			err = mySettings.SetSetting("ViewedContentNetworkType", networkTypeString)
			if (err != nil) { return err }
		}

		isStopped := CheckIfBuildViewedContentIsStopped()
		if (isStopped == true){
			return nil
		}

		appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "0.50")

		contentReadyStatus, err := GetViewedContentIsReadyStatus(networkType)
		if (err != nil) { return err }
		if (contentReadyStatus == false){

			// Now we sort content.

			currentSortByAttribute, err := GetViewedContentSortByAttribute()
			if (err != nil) { return err }
			currentSortDirection, err := GetViewedContentSortDirection()
			if (err != nil) { return err }

			currentContentsList, err := viewedContentListDatastore.GetList()
			if (err != nil) { return err }

			// We use this map to make sure there are no duplicate content hashes
			// This should never happen, unless the user's stored list was edited or there is a bug
			allContentHashesMap := make(map[string]struct{})

			//Map Structure: Content Hash Hex -> Content attribute value
			contentAttributeValuesMap := make(map[string]float64)

			maximumIndex := len(currentContentsList) - 1

			for index, contentHashHex := range currentContentsList{

				contentHash, err := encoding.DecodeHexStringToBytes(contentHashHex)
				if (err != nil){
					return errors.New("viewedContentList contains invalid content hash during sort: " + contentHashHex)
				}

				_, exists := allContentHashesMap[string(contentHash)]
				if (exists == true){
					return errors.New("viewedContentList contains duplicate content hash.")
				}
				allContentHashesMap[string(contentHash)] = struct{}{}

				// Outputs:
				//	-bool: Content attribute value is known
				//	-float64: Content attribute value
				//	-error
				getContentAttributeValue := func()(bool, float64, error){

					if (currentSortByAttribute == "Controversy"){
						requiredDataExists, controversyRating, err := contentControversy.GetContentControversyRating(contentHash)
						if (err != nil) { return false, 0, err }
						if (requiredDataExists == false){
							// We do not know the content controversy.
							return false, 0, nil
						}

						return true, float64(controversyRating), nil
					}
					if (currentSortByAttribute == "NumberOfReviewers"){

						contentType, err := helpers.GetContentTypeFromContentHash(contentHash)
						if (err != nil) { return false, 0, err }
						if (contentType == "Profile"){

							if (len(contentHash) != 28){
								return false, 0, errors.New("GetContentTypeFromContentHash returning Profile for different length contentHash: " + contentHashHex)
							}

							profileHash := [28]byte(contentHash)

							profileMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfProfileReviewers(profileHash)
							if (err != nil) { return false, 0, err }
							if (profileMetadataIsKnown == false || downloadingRequiredData == false){
								return false, 0, nil
							}

							return true, float64(numberOfReviewers), nil

						} else if (contentType == "Message"){

							if (len(contentHash) != 26){
								return false, 0, errors.New("GetContentTypeFromContentHash returning Message for different length contentHash: " + contentHashHex)
							}

							messageHash := [26]byte(contentHash)

							messageMetadataIsKnown, downloadingRequiredData, numberOfReviewers, err := reviewStorage.GetNumberOfMessageReviewers(messageHash)
							if (err != nil) { return false, 0, err }
							if (messageMetadataIsKnown == false || downloadingRequiredData == false){
								return false, 0, nil
							}

							return true, float64(numberOfReviewers), nil
						}

						return false, 0, errors.New("Viewed content list contains invalid contentHash during sort: " + contentHashHex)
					}

					//TODO: Add more attributes

					return false, 0, errors.New("GetViewedContentSortByAttribute returning unknown attribute: " + currentSortByAttribute)
				}

				contentAttributeValueIsKnown, contentAttributeValue, err := getContentAttributeValue()
				if (err != nil) { return err }
				if (contentAttributeValueIsKnown == true){
					contentAttributeValuesMap[contentHashHex] = contentAttributeValue
				}

				isStopped := CheckIfBuildViewedContentIsStopped()
				if (isStopped == true){
					return nil
				}

				newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80)
				if (err != nil) { return err }

				newProgressFloat := float64(newScaledPercentageInt)/100

				newProgressString := helpers.ConvertFloat64ToString(newProgressFloat)

				appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", newProgressString)
			}

			compareContentsFunction := func(contentHashA string, contentHashB string)int{

				if (contentHashA == contentHashB){
					panic("Duplicate content hashes called to compare function during sort.")
				}

				attributeValueA, attributeValueAExists := contentAttributeValuesMap[contentHashA]

				attributeValueB, attributeValueBExists := contentAttributeValuesMap[contentHashB]

				if (attributeValueAExists == false && attributeValueBExists == false){

					// We don't know the attribute value for either content
					// We sort contents in unicode order
					if (contentHashA < contentHashB){
						return -1
					}
					return 1

				} else if (attributeValueAExists == true && attributeValueBExists == false){

					// We sort unknown attribute contents to the back of the list

					return -1

				} else if (attributeValueAExists == false && attributeValueBExists == true){

					return 1
				}

				// Both attribute values exist

				if (attributeValueA == attributeValueB){

					// We sort content hashes in unicode order
					if (contentHashA < contentHashB){
						return -1
					}

					return 1
				}

				if (attributeValueA < attributeValueB){

					if (currentSortDirection == "Ascending"){
						return -1
					}
					return 1
				}
				if (currentSortDirection == "Ascending"){
					return 1
				}

				return -1
			}

			slices.SortFunc(currentContentsList, compareContentsFunction)

			err = viewedContentListDatastore.OverwriteList(currentContentsList)
			if (err != nil) { return err }

			err = mySettings.SetSetting("ViewedContentSortedStatus", "Yes")
			if (err != nil) { return err }
		}

		appMemory.SetMemoryEntry("ViewedContentReadyProgressStatus", "1")

		err = mySettings.SetSetting("ViewedContentNeedsRefreshYesNo", "No")
		if (err != nil) { return err }

		return nil
	}

	updateFunction := func(){

		err := updateViewedContent()
		if (err != nil){
			appMemory.SetMemoryEntry("ViewedContentBuildEncounteredError", "Yes")
			appMemory.SetMemoryEntry("ViewedContentBuildError", err.Error())
		}

		updatingViewedContentMutex.Unlock()
	}	

	go updateFunction()

	return nil
}

func GetNumberOfActiveViewedContentFilters()(int, error){

	numberOfActiveFilters := 0

	//TODO

	return numberOfActiveFilters, nil
}


func SetViewedContentFilterOnOffStatus(filterName string, newFilterStatus bool)error{

	filterOnOffStatusString := helpers.ConvertBoolToYesOrNoString(newFilterStatus)

	err := viewedContentFiltersMapDatastore.SetMapEntry(filterName, filterOnOffStatusString)
	if (err != nil) { return err }

	err = mySettings.SetSetting("ViewedContentGeneratedStatus", "No")
	if (err != nil) { return err }

	return nil
}

func GetViewedContentFilterOnOffStatus(filterName string)(bool, error){

	filterStatusExists, currentFilterStatus, err := viewedContentFiltersMapDatastore.GetMapEntry(filterName)
	if (err != nil) { return false, err }
	if (filterStatusExists == false){
		return false, nil
	}

	filterOnOffStatusBool, err := helpers.ConvertYesOrNoStringToBool(currentFilterStatus)
	if (err != nil) { return false, err }

	return filterOnOffStatusBool, nil
}






