package maidenhead

import (
	"fmt"
	"math"
)

const (
	// Earth radius
	r = 6371
)

var compassBearing = []struct {
	label        string
	start, ended float64
}{
	{"N", 000.00, 011.25}, {"NNE", 011.25, 033.75}, {"NE", 033.75, 056.25}, {"ENE", 056.25, 078.75},
	{"E", 078.75, 101.25}, {"ESE", 101.25, 123.75}, {"SE", 123.75, 146.25}, {"SSE", 146.25, 168.75},
	{"S", 168.75, 191.25}, {"SSW", 191.25, 213.75}, {"SW", 213.75, 236.25}, {"WSW", 236.25, 258.75},
	{"W", 258.75, 281.25}, {"WNW", 281.25, 303.75}, {"NW", 303.75, 326.25}, {"NNW", 326.25, 348.75},
	{"N", 348.75, 360.00},
}

// Point is a geographical point on the map.
type Point struct {
	Latitude  float64
	Longitude float64
}

// NewPoint returns a new Point structure with given latitude and longitude.
func NewPoint(latitude, longitude float64) Point {
	return Point{latitude, longitude}
}

// ParseLocator parses a Maidenhead Locator with permissive rule matching.
func ParseLocator(locator string) (Point, error) {
	return parseLocator(locator, false)
}

// ParseLocatorStrict parses a Maidenhead Locator with strict rule matching.
func ParseLocatorStrict(locator string) (Point, error) {
	return parseLocator(locator, true)
}

// EqualTo returns true if the coordinates point to the same geographical location.
func (p Point) EqualTo(other Point) bool {
	var (
		dlat = p.Latitude - other.Latitude
		dlng = p.Longitude - other.Longitude
	)

	for dlat < -180.0 {
		dlat += 360.0
	}
	for dlat > 180.0 {
		dlat -= 360.0
	}
	for dlng < -90.0 {
		dlng += 90.0
	}
	for dlng > 90.0 {
		dlng -= 90.0
	}

	return dlat == 0.0 && dlng == 0.0
}

// Bearing calculates the (approximate) bearing to another heading.
func (p Point) Bearing(heading Point) float64 {
	var (
		hn = p.Latitude / 180 * math.Pi
		he = p.Longitude / 180 * math.Pi
		n  = heading.Latitude / 180 * math.Pi
		e  = heading.Longitude / 180 * math.Pi
		co = math.Cos(he-e)*math.Cos(hn)*math.Cos(n) + math.Sin(hn)*math.Sin(n)
		ca = math.Atan(math.Abs(math.Sqrt(1-co*co) / co))
	)

	if co < 0.0 {
		ca = math.Pi - ca
	}

	var si = math.Sin(e-he) * math.Cos(n) * math.Cos(hn)
	co = math.Sin(n) - math.Sin(hn)*math.Cos(ca)
	var az = math.Atan(math.Abs(si / co))
	if co < 0.0 {
		az = math.Pi - az
	}
	if si < 0.0 {
		az = -az
	}
	if az < 0.0 {
		az = az + 2.0 + math.Pi
	}
	return az * 180 / math.Pi
}

// CompassBearing returns the compass bearing to a heading.
func (p Point) CompassBearing(heading Point) string {
	bearing := p.Bearing(heading)
	for bearing < 0.0 {
		bearing += 360.0
	}
	for bearing > 360.0 {
		bearing -= 360.0
	}

	for _, compass := range compassBearing {
		if bearing >= compass.start && bearing <= compass.ended {
			return compass.label
		}
	}

	// Should never reach
	return ""
}

// Distance calculates the (approximate) distance to another point in km.
func (p Point) Distance(other Point) float64 {
	var (
		hn = p.Latitude / 180 * math.Pi
		he = p.Longitude / 180 * math.Pi
		n  = other.Latitude / 180 * math.Pi
		e  = other.Longitude / 180 * math.Pi
		co = math.Cos(he-e)*math.Cos(hn)*math.Cos(n) + math.Sin(hn)*math.Sin(n)
		ca = math.Atan(math.Abs(math.Sqrt(1-co*co) / co))
	)

	if co < 0.0 {
		ca = math.Pi - ca
	}

	return r * ca
}

// GridSquare returns a Maidenhead Locator for the point coordinates.
func (p Point) GridSquare() (string, error) {
	return locator(p, SubSquarePrecision)
}

// Locator returns a Maidenhead Locator for the point coordinates with
// specified precision
func (p Point) Locator(precision int) (string, error) {
	return locator(p, precision)
}

// String returns a stringified Point structure.
func (p Point) String() string {
	return fmt.Sprintf("Point(%f, %f)", p.Latitude, p.Longitude)
}
