
// myMapList provides functions for managing user map lists
// Map lists are concurrency safe, and are stored in-memory and on disk.
// Examples of map lists include saved contacts, my chat messages, and my chat conversations

package myMapList

import "seekia/internal/localFilesystem"
import "seekia/internal/helpers"
import "seekia/internal/appMemory"

import "encoding/json"
import "path/filepath"
import "errors"
import "sync"
import "maps"


func InitializeMyMapListsFolder()error{

	userDirectory, err := localFilesystem.GetAppUserFolderPath()
	if (err != nil) { return err }

	myMapListsFolderpath := filepath.Join(userDirectory, "MyMapLists")

	_, err = localFilesystem.CreateFolder(myMapListsFolderpath)
	if (err != nil) { return err }

	return nil
}

type MyMapList struct{

	// Name of map list. The file name is derived from this name.
	mapListName string

	// Folderpath of the mapList file
	mapListFolderpath string

	// We lock this whenever we are editing the memory map and map file
	updatingMapListMutex sync.Mutex
	
	// We lock this whenever we edit the memoryMapList
	// memoryMutex only needs to be RLock/RUnlocked by GetMapList/GetMapListItems, because updatingMapListMutex is locked for all other reads
	memoryMutex sync.RWMutex

	// The map list stored in memory
	memoryMapList []map[string]string
}

func CreateNewMapList(newMapListName string)(*MyMapList, error){

	if (newMapListName == ""){
		return nil, errors.New("CreateNewMapList called with empty myMapList name")
	}

	var newMapList MyMapList

	newMapList.mapListName = newMapListName

	userDirectory, err := localFilesystem.GetAppUserFolderPath()
	if (err != nil) { return nil, err }

	mapListFolderpath := filepath.Join(userDirectory, "MyMapLists")

	newMapList.mapListFolderpath = mapListFolderpath

	getMapListFromFile := func()([]map[string]string, error){

		mapListFilepath := filepath.Join(mapListFolderpath, newMapListName + "MapList.json")

		fileExists, fileBytes, err := localFilesystem.GetFileContents(mapListFilepath)
		if (err != nil) { return nil, err }
		if (fileExists == false){
			emptyMapList := make([]map[string]string, 0)
			return emptyMapList, nil
		}

		currentMapList := make([]map[string]string, 0)

		err = json.Unmarshal(fileBytes, &currentMapList)
		if (err != nil) {
			return nil, errors.New("My Map list is corrupted: " + newMapListName)
		}

		return currentMapList, nil
	}

	mapListFromFile, err := getMapListFromFile()
	if (err != nil) { return nil, err }

	newMapList.memoryMapList = mapListFromFile

	return &newMapList, nil
}


//Outputs:
//	-[]map[string]string
//	-error
func (inputMapListObject *MyMapList) GetMapList()([]map[string]string, error){
	
	if (inputMapListObject == nil){
		return nil, errors.New("GetMapList called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return nil, errors.New("GetMapList called when no user is signed in.")
	}
	
	inputMapListObject.memoryMutex.RLock()

	currentMapList := inputMapListObject.memoryMapList

	mapListCopy := helpers.DeepCopyStringToStringMapList(currentMapList)

	inputMapListObject.memoryMutex.RUnlock()

	return mapListCopy, nil
}

// Note: Added items are not copied
func (inputMapListObject *MyMapList) AddMapListItem(newItem map[string]string)error{

	if (inputMapListObject == nil){
		return errors.New("AddMapListItem called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return errors.New("AddMapListItem called when no user is signed in.")
	}

	inputMapListObject.updatingMapListMutex.Lock()
	defer inputMapListObject.updatingMapListMutex.Unlock()

	inputMapListObject.memoryMutex.Lock()

	currentMapList := inputMapListObject.memoryMapList
	currentMapList = append(currentMapList, newItem)
	inputMapListObject.memoryMapList = currentMapList

	inputMapListObject.memoryMutex.Unlock()

	mapListFolderpath := inputMapListObject.mapListFolderpath
	mapListName := inputMapListObject.mapListName

	err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, currentMapList)
	if (err != nil) { return err }

	return nil
}

// This can be used to delete multiple entries at a time.
// Any maps that contain the same entries as the input map will be deleted
func (inputMapListObject *MyMapList) DeleteMapListItems(mapItem map[string]string)error{
	
	if (inputMapListObject == nil){
		return errors.New("DeleteMapListItems called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return errors.New("DeleteMapListItems called when no user is signed in.")
	}
	
	inputMapListObject.updatingMapListMutex.Lock()
	defer inputMapListObject.updatingMapListMutex.Unlock()
	
	currentMapList := inputMapListObject.memoryMapList
 
	if (len(currentMapList) == 0){
		return nil
	}

	newMapList := make([]map[string]string, 0)
	
	anyItemDeleted := false

	for _, element := range currentMapList{

		areEqual := checkIfMapContainsMapEntries(mapItem, element)
		if (areEqual == false){
			newMapList = append(newMapList, element)
			continue
		}

		anyItemDeleted = true
	}

	if (anyItemDeleted == false){
		// No items were deleted. Nothing left to do.
		return nil
	}

	inputMapListObject.memoryMutex.Lock()
	inputMapListObject.memoryMapList = newMapList
	inputMapListObject.memoryMutex.Unlock()

	mapListFolderpath := inputMapListObject.mapListFolderpath	
	mapListName := inputMapListObject.mapListName
	
	err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, newMapList)
	if (err != nil) { return err }
	
	return nil
}


//Outputs:
//	bool: Any item was found
//	[]map[string]string: list of matching items
//	error
func (inputMapListObject *MyMapList) GetMapListItems(lookupItem map[string]string)(bool, []map[string]string, error){

	if (inputMapListObject == nil){
		return false, nil, errors.New("GetMapListItems called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return false, nil, errors.New("GetMapListItems called when no user is signed in.")
	}
	
	inputMapListObject.memoryMutex.RLock()
	currentMapList := inputMapListObject.memoryMapList

	matchingItemsMapList := make([]map[string]string, 0)

	for _, element := range currentMapList{
	
		areEqual := checkIfMapContainsMapEntries(lookupItem, element)
		if (areEqual == true){

			matchingItemCopy := maps.Clone(element)

			matchingItemsMapList = append(matchingItemsMapList, matchingItemCopy)
		}

	}
	inputMapListObject.memoryMutex.RUnlock()

	if (len(matchingItemsMapList) == 0) {
		
		return false, matchingItemsMapList, nil
	}

	return true, matchingItemsMapList, nil
}

// Note: Input map list is not copied
func (inputMapListObject *MyMapList) OverwriteMapList(newMapList []map[string]string) error{

	if (inputMapListObject == nil){
		return errors.New("OverwriteMapList called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return errors.New("OverwriteMapList called when no user is signed in.")
	}

	inputMapListObject.updatingMapListMutex.Lock()
	defer inputMapListObject.updatingMapListMutex.Unlock()

	inputMapListObject.memoryMutex.Lock()
	inputMapListObject.memoryMapList = newMapList
	inputMapListObject.memoryMutex.Unlock()

	mapListFolderpath := inputMapListObject.mapListFolderpath
	mapListName := inputMapListObject.mapListName

	err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, newMapList)
	if (err != nil) { return err }

	return nil
}

func (inputMapListObject *MyMapList) DeleteMapList() error{

	if (inputMapListObject == nil){
		return errors.New("DeleteMapList called when MyMapList object is not initialized.")
	}

	exists, _ := appMemory.GetMemoryEntry("AppUser")
	if (exists == false){
		return errors.New("DeleteMapList called when no user is signed in.")
	}

	inputMapListObject.updatingMapListMutex.Lock()
	defer inputMapListObject.updatingMapListMutex.Unlock()

	emptyMapList := make([]map[string]string, 0)

	inputMapListObject.memoryMutex.Lock()
	inputMapListObject.memoryMapList = emptyMapList
	inputMapListObject.memoryMutex.Unlock()

	mapListFolderpath := inputMapListObject.mapListFolderpath
	mapListName := inputMapListObject.mapListName

	err := overwriteMapListFileWithMapList(mapListFolderpath, mapListName, emptyMapList)
	if (err != nil) { return err }

	return nil
}

// We use this function to find matching items to delete/retrieve
// Map2 must contain all of map1's entries.
// If any map2 entries do not match match1 entries, return false
// If map1 contains more entries than map2, return false
// Map2 is allowed to contain more entries than map1
func checkIfMapContainsMapEntries(map1 map[string]string, map2 map[string]string) bool {

	for key, value := range map1 {
		
		currentValue, exists := map2[key]
		if (exists == false) {
			return false
		}
		
		if (value != currentValue) {
			return false
		}
	}

	return true
}

func overwriteMapListFileWithMapList(mapListFolderpath string, mapListName string, inputMapList []map[string]string)error{
	
	fileContents, err := json.MarshalIndent(inputMapList, "", "\t")
	if (err != nil) { return err }

	mapListFileName := mapListName + "MapList.json"
	
	err = localFilesystem.CreateOrOverwriteFile(fileContents, mapListFolderpath, mapListFileName)
	if (err != nil) { return err }

	return nil
}


