
// This utility will create a .messagepack file containing the information for all cities from the Countries States Cities database
// We remove fields that we don't need and remove cities without latitude and longitude data.
// We do this to make the file smaller, and thus make Seekia a smaller download for users.
// We also remove duplicate coordinates from the data.
// This input file is 50.8 MB, the output file is 6.4 MB
// The output file should be placed in /internal/worldLocations.

// You have to download the database file to generate the output worldCities.messagepack file.
// We are not including the input file in the source code so that the source code download is smaller

// The database repository: https://github.com/dr5hn/countries-states-cities-database/
//
// We are using version 2.2 of the database
// File to download: v2.2.tar.gz
// Download Link: https://github.com/dr5hn/countries-states-cities-database/archive/refs/tags/v2.2.tar.gz
// v2.2.tar.gz SHA256 checksum: fc9ed9642906d8629059c06094ed1ce53ad4fa510991a6485ff3422962cff1b9
//
// First, you must extract the downloaded .tar.gz file.
//
// We use the file cities.json as the input to the createCitiesFile utility.
// cities.json SHA256 checksum: b82a21f2c8402041c00787131f206eb06d931d12d062f4a8a0f2b07075263c99
//
// Output file: worldCities.messagepack
// worldCities.messagepack SHA256 checksum: 0c42a4c51db7f42dcc71035e375f010a5f80c41cd89d87f581a93f3226bf650d

package main

import "seekia/internal/worldLocations"
import "seekia/internal/encoding"
import "seekia/internal/helpers"
import "seekia/internal/localFilesystem"

import messagepack "github.com/vmihailenco/msgpack/v5"

import "errors"
import "encoding/json"
import "log"

func main(){

	createCitiesFileFunction := func()error{

		type cityItem struct{

			// Name of the city
			Name string

			// Name of state city is in
			State_name string

			// Name of country city is in
			Country_name string

			Latitude string
			Longitude string
		}

		fileExists, fileContents, err := localFilesystem.GetFileContents("./cities.json")
		if (err != nil){ return err }
		if (fileExists == false){
			return errors.New("cities.json file was not found.")
		}

		var citiesList []cityItem

		err = json.Unmarshal(fileContents, &citiesList)
		if (err != nil){ return err }

		//Map Structure: Name in input file -> Name we replace it with in our new file
		countryNamesToReplaceMap := map[string]string{
			"Bonaire, Sint Eustatius and Saba": "Bonaire",
			"Cote D'Ivoire (Ivory Coast)": "Cote D'Ivoire",
			"Gambia The": "Gambia",
			"Virgin Islands (US)": "United States",
			"Papua new Guinea": "Papua New Guinea",
			"Antigua and Barbuda": "Antigua And Barbuda",
			"Hong Kong S.A.R.": "China",
			"Saint Kitts and Nevis": "Saint Kitts And Nevis",
			"Saint Vincent and the Grenadines": "Saint Vincent And The Grenadines",
			"Trinidad and Tobago": "Trinidad And Tobago",
		}

		allCountryObjectsList, err := worldLocations.GetAllCountryObjectsList()
		if (err != nil) { return err }

		// Map Structure: Country Name -> Country Identifier
		countryIdentifiersMap := make(map[string]int)

		for _, countryObject := range allCountryObjectsList{

			countryIdentifier := countryObject.Identifier
			countryNamesList := countryObject.NamesList

			for _, countryName := range countryNamesList{

				countryIdentifiersMap[countryName] = countryIdentifier
			}
		}

		// We encode a messagepack file with the cities:
		// It is a slice of messagepack-encoded slices
		// Each slice represents a city object
		// []{Name (string), State (string), Country Identifier (int), Latitude (float64), Longitude (float64)}

		newLocationsSlice := make([]messagepack.RawMessage, 0, 148402)

		type coordinatesObject struct{
			Latitude float64
			Longitude float64
		}
		// We use this map to make sure no duplicate coordinate pairs are added
		existingCoordinatesMap := make(map[coordinatesObject]struct{})

		for _, cityItem := range citiesList{

			cityName := cityItem.Name
			cityState := cityItem.State_name
			cityCountry := cityItem.Country_name

			//Outputs:
			//	-bool: Coordinates exist
			//	-float64: Latitude
			//	-float64: Longitude
			//	-error
			getCityCoordinates := func()(bool, float64, float64, error){

				cityLatitude := cityItem.Latitude
				cityLongitude := cityItem.Longitude

				cityLatitudeFloat64, err := helpers.ConvertStringToFloat64(cityLatitude)
				if (err != nil){ return false, 0, 0, err }

				cityLongitudeFloat64, err := helpers.ConvertStringToFloat64(cityLongitude)
				if (err != nil){ return false, 0, 0, err }

				if (cityLatitudeFloat64 == 0 && cityLongitudeFloat64 == 0){
					return false, 0, 0, nil
				}

				cityCoordinatesObject := coordinatesObject{
					Latitude: cityLatitudeFloat64,
					Longitude: cityLongitudeFloat64,
				}

				_, exists := existingCoordinatesMap[cityCoordinatesObject]
				if (exists == false){
					return true, cityLatitudeFloat64, cityLongitudeFloat64, nil 
				}

				// The database has a duplicate.
				// We will slightly shift the coordinate until it is not a duplicate

				for i := 1; i < 1000; i++{

					newCityLatitude := cityLatitudeFloat64 + (.00000001 * float64(i))

					newCityCoordinatesObject := coordinatesObject{
						Latitude: newCityLatitude,
						Longitude: cityLongitudeFloat64,
					}

					_, exists := existingCoordinatesMap[newCityCoordinatesObject]
					if (exists == false){
						return true, newCityLatitude, cityLongitudeFloat64, nil
					}
				}

				return false, 0, 0, errors.New("Too many coordinate collisions: " + cityName)
			}

			coordinatesExist, cityLatitude, cityLongitude, err := getCityCoordinates()
			if (err != nil) { return err }
			if (coordinatesExist == false){
				// This city has no coordinate info. Skip it.
				continue
			}

			getCountryName := func()string{

				newName, exists := countryNamesToReplaceMap[cityCountry]
				if (exists == false){
					return cityCountry
				}
				return newName
			}

			countryName := getCountryName()

			countryIdentifier, exists := countryIdentifiersMap[countryName]
			if (exists == false){
				return errors.New("countryIdentifiersMap missing countryName: " + countryName)
			}

			cityNameMessagepack, err := encoding.EncodeMessagePackBytes(cityName)
			if (err != nil) { return err }

			cityStateMessagepack, err := encoding.EncodeMessagePackBytes(cityState)
			if (err != nil) { return err }

			cityCountryIdentifierMessagepack, err := encoding.EncodeMessagePackBytes(countryIdentifier)
			if (err != nil) { return err }

			cityLatitudeMessagepack, err := encoding.EncodeMessagePackBytes(cityLatitude)
			if (err != nil) { return err }

			cityLongitudeMessagepack, err := encoding.EncodeMessagePackBytes(cityLongitude)
			if (err != nil) { return err }

			cityMessagepackSlice := []messagepack.RawMessage{cityNameMessagepack, cityStateMessagepack, cityCountryIdentifierMessagepack, cityLatitudeMessagepack, cityLongitudeMessagepack}

			cityEncodedMessagepack, err := encoding.EncodeMessagePackBytes(cityMessagepackSlice)
			if (err != nil){ return err }

			newLocationsSlice = append(newLocationsSlice, cityEncodedMessagepack)

			newCityCoordinatesObject := coordinatesObject{
				Latitude: cityLatitude,
				Longitude: cityLongitude,
			}

			existingCoordinatesMap[newCityCoordinatesObject] = struct{}{}
		}

		newFileBytes, err := encoding.EncodeMessagePackBytes(newLocationsSlice)
		if (err != nil){ return err }

		err = localFilesystem.CreateOrOverwriteFile(newFileBytes, "./", "worldCities.messagepack") 
		if (err != nil){ return err }

		return nil	
	}

	err := createCitiesFileFunction()
	if (err != nil){
		log.Println("Failed to create cities file. Reason: " + err.Error())
	} else {
		log.Println("Successfully created cities file!")	
	}
}

