// Package geo is the one place that turns Maidenhead locators into positions,
// and positions into distances and bearings.
//
// It exists because there were about to be three copies. These functions lived
// in package main, which the internal packages cannot import, so the PSK
// Reporter watcher had its geometry injected from main and the web publisher
// was about to grow its own. A bearing that disagrees with itself between two
// panels is the kind of fault nobody reports, because each screen looks
// plausible on its own.
package geo

import (
	"math"
	"strings"
)

// GridToLatLon parses a Maidenhead locator (4 or 6 characters) and returns the
// centre of that square in degrees. ok=false on malformed input.
func GridToLatLon(grid string) (lat, lon float64, ok bool) {
	g := strings.ToUpper(strings.TrimSpace(grid))
	if len(g) < 4 {
		return 0, 0, false
	}
	A := g[0] - 'A'
	B := g[1] - 'A'
	C := g[2] - '0'
	D := g[3] - '0'
	if A > 17 || B > 17 || C > 9 || D > 9 {
		return 0, 0, false
	}
	lon = -180 + float64(A)*20 + float64(C)*2
	lat = -90 + float64(B)*10 + float64(D)*1
	if len(g) >= 6 {
		E := g[4] - 'A'
		F := g[5] - 'A'
		if E <= 23 && F <= 23 {
			lon += float64(E)*(5.0/60.0) + 2.5/60.0
			lat += float64(F)*(2.5/60.0) + 1.25/60.0
			return lat, lon, true
		}
	}
	// 4-character locator: aim at the centre of the square.
	lon += 1
	lat += 0.5
	return lat, lon, true
}

// HaversineKm returns the great-circle distance between two positions in
// kilometres. Mean Earth radius 6371 km.
func HaversineKm(lat1, lon1, lat2, lon2 float64) float64 {
	const R = 6371.0
	rad := math.Pi / 180.0
	dLat := (lat2 - lat1) * rad
	dLon := (lon2 - lon1) * rad
	a := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(lat1*rad)*math.Cos(lat2*rad)*math.Sin(dLon/2)*math.Sin(dLon/2)
	return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}

// DistanceBetweenGrids is the distance in kilometres between two locators,
// ok=false when either cannot be parsed.
func DistanceBetweenGrids(a, b string) (km float64, ok bool) {
	lat1, lon1, ok1 := GridToLatLon(a)
	lat2, lon2, ok2 := GridToLatLon(b)
	if !ok1 || !ok2 {
		return 0, false
	}
	return HaversineKm(lat1, lon1, lat2, lon2), true
}

// LatLonToGrid returns the 4-character Maidenhead square for a position.
func LatLonToGrid(lat, lon float64) string {
	lon = math.Mod(lon+180, 360)
	if lon < 0 {
		lon += 360
	}
	lat = lat + 90
	if lat < 0 {
		lat = 0
	} else if lat > 180 {
		lat = 180
	}
	return string([]byte{
		byte('A' + int(lon/20)),
		byte('A' + int(lat/10)),
		byte('0' + int(math.Mod(lon, 20)/2)),
		byte('0' + int(math.Mod(lat, 10)/1)),
	})
}

// NeighbourGrids returns the square holding (lat, lon) and the ring of squares
// around it — 9 squares for ring 1, 25 for ring 2.
//
// Used to filter the PSK Reporter feed at the BROKER rather than in OpsLog. A
// square is about 111 km tall and 150 km wide at mid latitudes, so one ring is
// roughly the 300 km "around here" the feed already meant, and the traffic that
// used to be received and discarded is never sent.
func NeighbourGrids(lat, lon float64, ring int) []string {
	if ring < 0 {
		ring = 0
	}
	seen := map[string]bool{}
	out := []string{}
	for dLat := -ring; dLat <= ring; dLat++ {
		for dLon := -ring; dLon <= ring; dLon++ {
			// One square step: 1° of latitude, 2° of longitude.
			la := lat + float64(dLat)
			lo := lon + float64(dLon)*2
			if la > 90 || la < -90 {
				continue // past a pole there is no square, not a wrapped one
			}
			g := LatLonToGrid(la, lo)
			if !seen[g] {
				seen[g] = true
				out = append(out, g)
			}
		}
	}
	return out
}
