package humanize

import (
	"fmt"
	"math"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/goodsign/monday"
	"github.com/jeandeaual/go-locale"
	"golang.org/x/text/language"
	"golang.org/x/text/message"
)

var cachedSystemLocale = ""

// Numericf produces a string from of the given number with give fixed precision
// in base 10 with thousands separators after every three orders of magnitude
// using thousands and decimal separator according to LC_NUMERIC; defaulting "en".
//
// e.g. Numericf(834142.32, 2) -> "834,142.32"
func Numericf(value float64, precision int) string {
	return f(value, precision, "LC_NUMERIC", true)
}

// Monetaryf produces a string from of the given number give minimum precision
// in base 10 with thousands separators after every three orders of magnitude
// using thousands and decimal separator according to LC_MONETARY; defaulting "en".
//
// e.g. Monetaryf(834142.3256, 2) -> "834,142.3256"
func Monetaryf(value float64, precision int) string {
	return f(value, precision, "LC_MONETARY", false)
}

// FixedMonetaryf produces a fixed-precision monetary-value string. See Monetaryf.
func FixedMonetaryf(value float64, precision int) string {
	return f(value, precision, "LC_MONETARY", true)
}

// borrowed from go-locale/util.go
func splitLocale(locale string) (string, string) {
	// Remove the encoding, if present
	formattedLocale := strings.Split(locale, ".")[0]
	// Normalize by replacing the hyphens with underscores
	formattedLocale = strings.Replace(formattedLocale, "-", "_", -1)

	// Split at the underscore
	split := strings.Split(formattedLocale, "_")
	language := split[0]
	territory := ""
	if len(split) > 1 {
		territory = split[1]
	}

	return language, territory
}

// GetLocale returns the current locale as defined in IETF BCP 47 (e.g. "en-US").
// The envvar provided is checked first (eg LC_TIME), before the platform-specific defaults.
func getLocale(envvar string) string {
	userLocale := "en-US" // default language-REGION
	// First try looking up envar directly
	envlang, ok := os.LookupEnv(envvar)
	if ok {
		language, region := splitLocale(envlang)
		userLocale = language
		if len(region) > 0 {
			userLocale = strings.Join([]string{language, region}, "-")
		}
	} else {
		// Then use (cached) system-specific locale
		if cachedSystemLocale == "" {
			if loc, err := locale.GetLocale(); err == nil {
				userLocale = loc
				cachedSystemLocale = loc
			}
		} else {
			userLocale = cachedSystemLocale
		}
	}
	return userLocale
}

// formatTimeExplicit formats the given time using the prescribed layout with the provided userLocale
func formatTimeExplicit(time time.Time, layout string, userLocale string) string {
	mondayLocale := monday.Locale(strings.Replace(userLocale, "-", "_", 1))
	return monday.Format(time, layout, mondayLocale)
}

// FormatTime is a dropin replacement time.Format(layout) that uses system locale + LC_TIME
func FormatTime(time time.Time, layout string) string {
	return formatTimeExplicit(time, layout, getLocale("LC_TIME"))
}

// f formats given value, with precision decimal places using thousands and decimal
// separator according to language found in given locale environment variable e.
// If fixed is true the decimal places are fixed to the given precision otherwise d is the
// minimum of decimal places until the first 0.
func f(value float64, precision int, envvar string, fixed bool) string {
	parts := strings.Split(strconv.FormatFloat(value, 'f', -1, 64), ".")
	if !fixed && len(parts) > 1 {
		for ; precision < len(parts[1]); precision += 1 {
			if parts[1][precision] == '0' {
				break
			}
		}
	}

	envlang, ok := os.LookupEnv(envvar)
	if !ok {
		envlang = "en"
	}
	lang := language.Make(envlang)

	format := fmt.Sprintf("%%.%df", precision)
	return message.NewPrinter(lang).Sprintf(format, value)
}

// Scale returns a scaled-down version of value and a suffix to add (M,B,etc.)
func Scale(value float64) (float64, string) {
	type scalingUnit struct {
		value  float64
		suffix string
	}

	//  quadrillion, quintrillion, sextillion, septillion, octillion, nonillion, and decillion
	var scales = [...]scalingUnit{
		{value: 1e12, suffix: "T"},
		{value: 1e9, suffix: "B"},
		{value: 1e6, suffix: "M"},
		{value: 1e3, suffix: "K"},
	}

	for _, scale := range scales {
		if math.Abs(value) > scale.value {
			return value / scale.value, scale.suffix
		}
	}
	return value, ""
}

// ScaleNumericf scales a large number down using a suffix, then formats it with the
// prescribed number of significant digits.
func ScaleNumericf(value float64, digits int) string {
	value, suffix := Scale(value)

	// Round the scaled value to a certain number of significant figures
	var s string
	if math.Abs(value) < 1 {
		s = Numericf(value, digits)
	} else {
		numDigits := len(fmt.Sprintf("%.0f", math.Abs(value)))
		if numDigits >= digits {
			s = Numericf(value, 0)
		} else {
			s = Numericf(value, digits-numDigits)
		}
	}

	return s + suffix
}
