package models

import (
	"fmt"
	"math"
	"net/http"
	"strconv"

	"code.nonshy.com/nonshy/website/pkg/geoip"
	"code.nonshy.com/nonshy/website/pkg/log"
)

// UserLocation table holds a user's location preference and coordinates.
type UserLocation struct {
	UserID    uint64 `gorm:"primaryKey"`
	Source    string
	Latitude  float64 `gorm:"index"`
	Longitude float64 `gorm:"index"`
}

// Source options for UserLocation.
const (
	LocationSourceNone  = ""
	LocationSourceGeoIP = "geoip"
	LocationSourceGPS   = "gps"
	LocationSourcePin   = "pin"
)

// GetUserLocation gets the UserLocation object for a user ID, or a new object.
func GetUserLocation(userId uint64) *UserLocation {
	var ul = &UserLocation{}
	result := DB.First(&ul, userId)
	if result.Error != nil {
		return &UserLocation{
			UserID: userId,
		}
	}
	return ul
}

// Save the UserLocation.
func (ul *UserLocation) Save() error {
	if ul.Source == LocationSourceNone {
		ul.Latitude = 0
		ul.Longitude = 0
	}
	return DB.Save(ul).Error
}

// RefreshGeoIP will auto-update a user's location by GeoIP if that's their setting.
func RefreshGeoIP(userID uint64, r *http.Request) (*UserLocation, error) {
	loc := GetUserLocation(userID)
	if loc.Source == LocationSourceGeoIP {
		if insights, err := geoip.GetRequestInsights(r); err == nil {
			loc.Latitude = truncate(insights.Latitude)
			loc.Longitude = truncate(insights.Longitude)
			return loc, loc.Save()
		} else {
			return loc, fmt.Errorf("didn't get insights: %s", err)
		}
	}
	return loc, nil
}

func truncate(f float64) float64 {
	s := strconv.FormatFloat(f, 'f', 2, 64)
	f, _ = strconv.ParseFloat(s, 64)
	return f
}

// MapDistances computes human readable distances between you and the set of users.
func MapDistances(currentUser *User, others []*User) DistanceMap {
	// Get all the distances we can.
	var (
		result = DistanceMap{}
		myDist = GetUserLocation(currentUser.ID)
		// dist    = map[uint64]*UserLocation{}
		userIDs = []uint64{}
	)
	for _, user := range others {
		userIDs = append(userIDs, user.ID)
	}

	// Query for their UserLocation objects, if exists.
	var ul = []*UserLocation{}
	res := DB.Where("user_id IN ?", userIDs).Find(&ul)
	if res.Error != nil {
		log.Error("MapDistances: %s", res.Error)
		return result
	}

	// Map them out.
	for _, row := range ul {
		km, mi := HaversineDistance(
			myDist.Latitude, myDist.Longitude,
			row.Latitude, row.Longitude,
		)
		result[row.UserID] = fmt.Sprintf("%.1fkm / %.1fmi", km, mi)
	}

	return result
}

// DistanceMap maps user IDs to distance strings.
type DistanceMap map[uint64]string

// Get a value from the DistanceMap for easy front-end access.
func (dm DistanceMap) Get(key uint64) string {
	if value, ok := dm[key]; ok {
		return value
	}
	return "unknown distance"
}

// HaversineDistance returns the distance (in kilometers, miles) between
// two points of latitude and longitude pairs.
func HaversineDistance(lat1, lon1, lat2, lon2 float64) (float64, float64) {
	lat1 *= piRad
	lon1 *= piRad
	lat2 *= piRad
	lon2 *= piRad
	var r = earthRadius

	// Calculate.
	h := hsin(lat2-lat1) + math.Cos(lat1)*math.Cos(lat2)*hsin(lon2-lon1)

	meters := 2 * r * math.Asin(math.Sqrt(h))
	kilometers := meters / 1000
	miles := kilometers * 0.621371
	return kilometers, miles
}

// adapted from: https://gist.github.com/cdipaolo/d3f8db3848278b49db68
// haversin(θ) function
func hsin(theta float64) float64 {
	return math.Pow(math.Sin(theta/2), 2)
}

const (
	piRad       = math.Pi / 180
	earthRadius = 6378100.0 // Earth radius in meters
)
