// Package maidenhead implements the Maidenhead Locator System, a geographic
// coordinate system used by amataur radio (HAM) operators.
package maidenhead

import (
	"errors"
	"fmt"
	"math"
	"strings"
)

// Precision of the computed locator.
const (
	FieldPrecision = iota + 1
	SquarePrecision
	SubSquarePrecision
	ExtendedSquarePrecision
)

var (
	upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
	lower = "abcdefghijklmnopqrstuvwx"
	digit = "0123456789"
)

// locator computes the Maidenhead Locator for a given position.
func locator(p Point, precision int) (string, error) {
	if math.IsNaN(p.Latitude) {
		return "", errors.New("maidenhead: latitude is not a digit")
	}
	if math.IsInf(p.Latitude, 0) {
		return "", errors.New("maidenhead: latitude is infinite")
	}
	if math.IsNaN(p.Longitude) {
		return "", errors.New("maidenhead: longitude is not a digit")
	}
	if math.IsInf(p.Longitude, 0) {
		return "", errors.New("maidenhead: longitude is infinite")
	}
	if math.Abs(p.Latitude) == 90.0 {
		return "", errors.New("maidenhead: grid square invalid at poles")
	} else if math.Abs(p.Latitude) > 90.0 {
		return "", fmt.Errorf("maidenhead: invalid latitude %.04f", p.Latitude)
	} else if math.Abs(p.Longitude) > 180.0 {
		return "", fmt.Errorf("maidenhead: invalid longitude %.05f", p.Longitude)
	}

	var (
		lat = p.Latitude + 90.0
		lng = p.Longitude + 180.0
		loc string
	)

	lat = lat/10.0 + 0.0000001
	lng = lng/20.0 + 0.0000001
	loc = loc + string(upper[int(lng)]) + string(upper[int(lat)])
	if precision == 1 {
		return loc, nil
	}
	lat = 10 * (lat - math.Floor(lat))
	lng = 10 * (lng - math.Floor(lng))
	loc = loc + fmt.Sprintf("%d%d", int(lng)%10, int(lat)%10)
	if precision == 2 {
		return loc, nil
	}
	lat = 24 * (lat - math.Floor(lat))
	lng = 24 * (lng - math.Floor(lng))
	loc = loc + string(upper[int(lng)]) + string(upper[int(lat)])
	if precision == 3 {
		return loc, nil
	}
	lat = 10 * (lat - math.Floor(lat))
	lng = 10 * (lng - math.Floor(lng))
	loc = loc + fmt.Sprintf("%d%d", int(lng)%10, int(lat)%10)
	if precision == 4 {
		return loc, nil
	}
	lat = 24 * (lat - math.Floor(lat))
	lng = 24 * (lng - math.Floor(lng))
	loc = loc + string(lower[int(lng)]) + string(lower[int(lat)])
	return loc, nil
}

var parseLocatorMult = []struct {
	s, p string
	mult float64
}{
	{upper[:18], lower[:18], 20.0},
	{upper[:18], lower[:18], 10.0},
	{digit[:10], digit[:10], 20.0 / 10.0},
	{digit[:10], digit[:10], 10.0 / 10.0},
	{upper[:24], lower[:24], 20.0 / (10.0 * 24.0)},
	{upper[:24], lower[:24], 10.0 / (10.0 * 24.0)},
	{digit[:10], digit[:10], 20.0 / (10.0 * 24.0 * 10.0)},
	{digit[:10], digit[:10], 10.0 / (10.0 * 24.0 * 10.0)},
	{lower[:24], lower[:24], 20.0 / (10.0 * 24.0 * 10.0 * 24.0)},
	{lower[:24], lower[:24], 10.0 / (10.0 * 24.0 * 10.0 * 24.0)},
}

var maxLocatorLength = len(parseLocatorMult)

func parseLocator(locator string, strict bool) (point Point, err error) {
	var (
		lnglat = [2]float64{
			-180.0,
			-90.0,
		}
		j int
	)

	if len(locator) > maxLocatorLength {
		err = fmt.Errorf("maidenhead: locator is too long (%d characters, maximum %d characters allowed)",
			len(locator), maxLocatorLength)
		return
	}

	if len(locator)%2 != 0 {
		err = fmt.Errorf("maidenhead: locator has odd number of characters")
		return
	}

	if strict {
		for i, char := range locator {
			if j = strings.Index(parseLocatorMult[i].s, string(char)); j < 0 {
				err = fmt.Errorf("maidenhead: invalid character at offset %d", i)
				return
			}
			lnglat[i%2] += float64(j) * parseLocatorMult[i].mult
		}
	} else {
		for i, char := range strings.ToLower(locator) {
			if j = strings.Index(parseLocatorMult[i].p, string(char)); j < 0 {
				err = fmt.Errorf("maidenhead: invalid character at offset %d", i)
				return
			}
			lnglat[i%2] += float64(j) * parseLocatorMult[i].mult
		}
	}

	point = NewPoint(lnglat[1], lnglat[0])
	return
}
