// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package geo

import (
	"math"
	"strconv"
	"strings"
	"unicode"
)

const (
	Degree Degrees  = 1
	Radian Radians  = 1
	Turn   Turns    = 1
	Meter  Distance = 1
)

// Degrees represents a latitude or longitude, in decimal degrees.
type Degrees float64

// ParseDegrees parses s as decimal degrees.
func ParseDegrees(s string) (Degrees, error) {
	s = strings.TrimSuffix(s, "°")
	f, err := strconv.ParseFloat(s, 64)
	return Degrees(f), err
}

// MustParseDegrees parses s as decimal degrees, but panics on error.
func MustParseDegrees(s string) Degrees {
	d, err := ParseDegrees(s)
	if err != nil {
		panic(err)
	}
	return d
}

// String implements the [Stringer] interface. The output is formatted in
// decimal degrees, prefixed by either the appropriate + or - sign, and suffixed
// by a ° degree symbol.
func (d Degrees) String() string {
	b, _ := d.AppendText(nil)
	b = append(b, []byte("°")...)
	return string(b)
}

// AppendText implements [encoding.TextAppender]. The output is formatted in
// decimal degrees, prefixed by either the appropriate + or - sign.
func (d Degrees) AppendText(b []byte) ([]byte, error) {
	b = d.AppendZeroPaddedText(b, 0)
	return b, nil
}

// AppendZeroPaddedText appends d formatted as decimal degrees to b. The number of
// integer digits will be zero-padded to nint.
func (d Degrees) AppendZeroPaddedText(b []byte, nint int) []byte {
	n := float64(d)

	if math.IsInf(n, 0) || math.IsNaN(n) {
		return strconv.AppendFloat(b, n, 'f', -1, 64)
	}

	sign := byte('+')
	if math.Signbit(n) {
		sign = '-'
		n = -n
	}
	b = append(b, sign)

	pad := nint - 1
	for nn := n / 10; nn >= 1 && pad > 0; nn /= 10 {
		pad--
	}
	for range pad {
		b = append(b, '0')
	}
	return strconv.AppendFloat(b, n, 'f', -1, 64)
}

// Radians converts d into radians.
func (d Degrees) Radians() Radians {
	return Radians(d * math.Pi / 180.0)
}

// Turns converts d into a number of turns.
func (d Degrees) Turns() Turns {
	return Turns(d / 360.0)
}

// Radians represents a latitude or longitude, in radians.
type Radians float64

// ParseRadians parses s as radians.
func ParseRadians(s string) (Radians, error) {
	s = strings.TrimSuffix(s, "rad")
	s = strings.TrimRightFunc(s, unicode.IsSpace)
	f, err := strconv.ParseFloat(s, 64)
	return Radians(f), err
}

// MustParseRadians parses s as radians, but panics on error.
func MustParseRadians(s string) Radians {
	r, err := ParseRadians(s)
	if err != nil {
		panic(err)
	}
	return r
}

// String implements the [Stringer] interface.
func (r Radians) String() string {
	return strconv.FormatFloat(float64(r), 'f', -1, 64) + " rad"
}

// Degrees converts r into decimal degrees.
func (r Radians) Degrees() Degrees {
	return Degrees(r * 180.0 / math.Pi)
}

// Turns converts r into a number of turns.
func (r Radians) Turns() Turns {
	return Turns(r / 2 / math.Pi)
}

// Turns represents a number of complete revolutions around a sphere.
type Turns float64

// String implements the [Stringer] interface.
func (o Turns) String() string {
	return strconv.FormatFloat(float64(o), 'f', -1, 64)
}

// Degrees converts t into decimal degrees.
func (o Turns) Degrees() Degrees {
	return Degrees(o * 360.0)
}

// Radians converts t into radians.
func (o Turns) Radians() Radians {
	return Radians(o * 2 * math.Pi)
}

// Distance represents a great-circle distance in meters.
type Distance float64

// ParseDistance parses s as distance in meters.
func ParseDistance(s string) (Distance, error) {
	s = strings.TrimSuffix(s, "m")
	s = strings.TrimRightFunc(s, unicode.IsSpace)
	f, err := strconv.ParseFloat(s, 64)
	return Distance(f), err
}

// MustParseDistance parses s as distance in meters, but panics on error.
func MustParseDistance(s string) Distance {
	d, err := ParseDistance(s)
	if err != nil {
		panic(err)
	}
	return d
}

// String implements the [Stringer] interface.
func (d Distance) String() string {
	return strconv.FormatFloat(float64(d), 'f', -1, 64) + "m"
}

// DistanceOnEarth converts t turns into the great-circle distance, in meters.
func DistanceOnEarth(t Turns) Distance {
	return Distance(t) * EarthMeanCircumference
}

// Earth Fact Sheet
// https://nssdc.gsfc.nasa.gov/planetary/factsheet/earthfact.html
const (
	// EarthMeanRadius is the volumetric mean radius of the Earth.
	EarthMeanRadius = 6_371_000 * Meter
	// EarthMeanCircumference is the volumetric mean circumference of the Earth.
	EarthMeanCircumference = 2 * math.Pi * EarthMeanRadius

	// earthEquatorialRadius is the equatorial radius of the Earth.
	earthEquatorialRadius = 6_378_137 * Meter
	// earthEquatorialCircumference is the equatorial circumference of the Earth.
	earthEquatorialCircumference = 2 * math.Pi * earthEquatorialRadius

	// earthPolarRadius is the polar radius of the Earth.
	earthPolarRadius = 6_356_752 * Meter
	// earthPolarCircumference is the polar circumference of the Earth.
	earthPolarCircumference = 2 * math.Pi * earthPolarRadius
)
