package ut

import (
	"errors"
	"fmt"
	"log"
	"math"
	"regexp"
	"strconv"
	"strings"
	"sync"
)

// numberFormat is a struct that contains all the information about number
// formatting for a specific locale that we need to do number, currency, and
// percentage formatting
type numberFormat struct {
	positivePrefix   string
	positiveSuffix   string
	negativePrefix   string
	negativeSuffix   string
	multiplier       int
	minDecimalDigits int
	maxDecimalDigits int
	minIntegerDigits int
	groupSizeFinal   int // only the right-most (least significant) group
	groupSizeMain    int // all other groups
}

// CurrencyType is the type of Currency your converting
type CurrencyType int

// Currency Types such as Standard vs Accounting notation i.e. -$123.50 vs ($123.50)
const (
	CurrencyStandard CurrencyType = iota
	CurrencyAccounting
)

var (
	// numberFormats keeps a copy of all numberFormat instances that have been
	// loaded before, to prevent parsing a single number format string multiple
	// times. There is vey little danger of this list consuming too much memory,
	// since the data for each of these is pretty small in size, and the same
	// formats are used by multiple locales.
	numberFormats           = map[string]*numberFormat{}
	numberFormatsNoDecimals = map[string]*numberFormat{}

	nfMutex   = new(sync.RWMutex)
	nfndMutex = new(sync.RWMutex)

	// prefixSuffixRegex is a regular expression that is used to parse number
	// formats
	prefixSuffixRegex = regexp.MustCompile(`(.*?)[#,\.0]+(.*)`)
)

func getCurrencyPattern(typ CurrencyType, nf NumberFormats) string {

	if typ == CurrencyStandard {
		return nf.Currency
	}

	return nf.CurrencyAccounting
}

// FmtCurrency takes a float number and a currency key and returns a string
// with a properly formatted currency amount with the correct currency symbol.
// If a symbol cannot be found for the reqested currency, this will return blank,
// use FmtCurrencySafe for variant.
func (n Number) FmtCurrency(typ CurrencyType, currency string, number float64) string {

	formatted, err := n.FmtCurrencySafe(typ, currency, number)
	if err != nil {
		fmt.Println(err)
	}

	return formatted
}

// FmtCurrencySafe takes a float number and a currency key and returns a string
// with a properly formatted currency amount with the correct currency symbol.
// If a symbol cannot be found for the reqested currency, the the key is used
// instead. If the currency key requested is not recognized, it is used as the
// symbol, and an error is returned with the formatted string.
func (n Number) FmtCurrencySafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {

	format := n.parseFormat(getCurrencyPattern(typ, n.Formats), true)
	result := n.formatNumber(format, number)

	c, ok := n.Currencies[currency]
	if !ok {
		s := "**** WARNING **** unknown currency: " + currency
		err = errors.New(s)
		log.Println(s)
		formatted = strings.Replace(result, "¤", currency, -1)
		return
	}

	formatted = strings.Replace(result, "¤", c.Symbol, -1)
	return
}

// FmtCurrencyWhole does exactly what FormatCurrency does, but it leaves off
// any decimal places. AKA, it would return $100 rather than $100.00.
// If a symbol cannot be found for the reqested currency, this will panic, use
// FmtCurrencyWholeSafe for non panicing variant.
func (n Number) FmtCurrencyWhole(typ CurrencyType, currency string, number float64) string {

	formatted, err := n.FmtCurrencyWholeSafe(typ, currency, number)
	if err != nil {
		fmt.Println(err)
	}

	return formatted
}

// FmtCurrencyWholeSafe does exactly what FormatCurrency does, but it leaves off
// any decimal places. AKA, it would return $100 rather than $100.00.
func (n Number) FmtCurrencyWholeSafe(typ CurrencyType, currency string, number float64) (formatted string, err error) {
	format := n.parseFormat(getCurrencyPattern(typ, n.Formats), false)
	result := n.formatNumber(format, number)

	c, ok := n.Currencies[currency]
	if !ok {
		s := "**** WARNING **** unknown currency: " + currency
		err = errors.New(s)
		log.Println(s)
		formatted = strings.Replace(result, "¤", currency, -1)
		return
	}

	formatted = strings.Replace(result, "¤", c.Symbol, -1)
	return
}

// FmtNumber takes a float number and returns a properly formatted string
// representation of that number according to the locale's number format.
func (n Number) FmtNumber(number float64) string {
	return n.formatNumber(n.parseFormat(n.Formats.Decimal, true), number)
}

// FmtNumberWhole does exactly what FormatNumber does, but it leaves off any
// decimal places. AKA, it would return 100 rather than 100.01.
func (n Number) FmtNumberWhole(number float64) string {
	return n.formatNumber(n.parseFormat(n.Formats.Decimal, false), number)
}

// FmtPercent takes a float number and returns a properly formatted string
// representation of that number as a percentage according to the locale's
// percentage format.
func (n Number) FmtPercent(number float64) string {
	return n.formatNumber(n.parseFormat(n.Formats.Percent, true), number)
}

// parseFormat takes a format string and returns a numberFormat instance
func (n Number) parseFormat(pattern string, includeDecimalDigits bool) *numberFormat {

	if includeDecimalDigits {

		nfMutex.RLock()

		if format, exists := numberFormats[pattern]; exists {
			nfMutex.RUnlock()
			return format
		}

		nfMutex.RUnlock()

	} else {

		nfndMutex.RLock()

		if format, exists := numberFormatsNoDecimals[pattern]; exists {
			nfndMutex.RUnlock()
			return format
		}

		nfndMutex.RUnlock()
	}

	format := new(numberFormat)
	patterns := strings.Split(pattern, ";")

	matches := prefixSuffixRegex.FindAllStringSubmatch(patterns[0], -1)
	if len(matches) > 0 {
		if len(matches[0]) > 1 {
			format.positivePrefix = matches[0][1]
		}
		if len(matches[0]) > 2 {
			format.positiveSuffix = matches[0][2]
		}
	}

	// default values for negative prefix & suffix
	format.negativePrefix = string(n.Symbols.Negative) + string(format.positivePrefix)
	format.negativeSuffix = format.positiveSuffix

	// see if they are in the pattern
	if len(patterns) > 1 {
		matches = prefixSuffixRegex.FindAllStringSubmatch(patterns[1], -1)

		if len(matches) > 0 {
			if len(matches[0]) > 1 {
				format.negativePrefix = matches[0][1]
			}
			if len(matches[0]) > 2 {
				format.negativeSuffix = matches[0][2]
			}
		}
	}

	pat := patterns[0]

	if strings.Index(pat, "%") != -1 {
		format.multiplier = 100
	} else if strings.Index(pat, "‰") != -1 {
		format.multiplier = 1000
	} else {
		format.multiplier = 1
	}

	pos := strings.Index(pat, ".")

	if pos != -1 {
		pos2 := strings.LastIndex(pat, "0")
		if pos2 > pos {
			format.minDecimalDigits = pos2 - pos
		}

		pos3 := strings.LastIndex(pat, "#")
		if pos3 >= pos2 {
			format.maxDecimalDigits = pos3 - pos
		} else {
			format.maxDecimalDigits = format.minDecimalDigits
		}

		pat = pat[0:pos]
	}

	p := strings.Replace(pat, ",", "", -1)
	pos = strings.Index(p, "0")
	if pos != -1 {
		format.minIntegerDigits = strings.LastIndex(p, "0") - pos + 1
	}

	p = strings.Replace(pat, "#", "0", -1)
	pos = strings.LastIndex(pat, ",")
	if pos != -1 {
		format.groupSizeFinal = strings.LastIndex(p, "0") - pos
		pos2 := strings.LastIndex(p[0:pos], ",")
		if pos2 != -1 {
			format.groupSizeMain = pos - pos2 - 1
		} else {
			format.groupSizeMain = format.groupSizeFinal
		}
	}

	if includeDecimalDigits {
		nfMutex.Lock()
		numberFormats[pattern] = format
		nfMutex.Unlock()
		return format
	}

	format.maxDecimalDigits = 0
	format.minDecimalDigits = 0
	nfndMutex.Lock()
	numberFormatsNoDecimals[pattern] = format
	nfndMutex.Unlock()
	return format
}

// formatNumber takes an arbitrary numberFormat and a number and applies that
// format to that number, returning the resulting string
func (n Number) formatNumber(format *numberFormat, number float64) string {

	negative := number < 0

	// apply the multiplier first - this is mainly used for percents
	value := math.Abs(number * float64(format.multiplier))
	stringValue := ""

	// get the initial string value, with the maximum # decimal digits
	if format.maxDecimalDigits >= 0 {
		stringValue = numberRound(value, format.maxDecimalDigits)
	} else {
		stringValue = fmt.Sprintf("%f", value)
	}

	// separate the integer from the decimal parts
	pos := strings.Index(stringValue, ".")
	integer := stringValue
	decimal := ""
	if pos != -1 {
		integer = stringValue[:pos]
		decimal = stringValue[pos+1:]
	}

	// make sure the minimum # decimal digits are there
	for len(decimal) < format.minDecimalDigits {
		decimal = decimal + "0"
	}

	// make sure the minimum # integer digits are there
	for len(integer) < format.minIntegerDigits {
		integer = "0" + integer
	}

	// if there's a decimal portion, prepend the decimal point symbol
	if len(decimal) > 0 {
		decimal = string(n.Symbols.Decimal) + decimal
	}

	// put the integer portion into properly sized groups
	if format.groupSizeFinal > 0 && len(integer) > format.groupSizeFinal {
		if len(integer) > format.groupSizeMain {
			groupFinal := integer[len(integer)-format.groupSizeFinal:]
			groupFirst := integer[:len(integer)-format.groupSizeFinal]
			integer = strings.Join(
				chunkString(groupFirst, format.groupSizeMain),
				n.Symbols.Group,
			) + n.Symbols.Group + groupFinal
		}
	}

	// append/prepend negative/positive prefix/suffix
	formatted := ""
	if negative {
		formatted = format.negativePrefix + integer + decimal + format.negativeSuffix
	} else {
		formatted = format.positivePrefix + integer + decimal + format.positiveSuffix
	}

	// replace percents and permilles with the local symbols (likely to be exactly the same)
	formatted = strings.Replace(formatted, "%", string(n.Symbols.Percent), -1)
	formatted = strings.Replace(formatted, "‰", string(n.Symbols.PerMille), -1)

	return formatted
}

// chunkString takes a string and chunks it into size-sized pieces in a slice.
// If the length of the string is not divisible by the size, then the first
// chunk in the slice will be padded to compensate.
func chunkString(str string, size int) []string {
	if str == "" {
		return []string{}
	}

	if size == 0 {
		return []string{str}
	}

	chunks := make([]string, int64(math.Ceil(float64(len(str))/float64(size))))

	for len(str) < len(chunks)*size {
		str = " " + str
	}

	for i := 0; i < len(chunks); i++ {
		start := i * size
		stop := int64(math.Min(float64(start+size), float64(len(str))))
		chunks[i] = str[start:stop]
	}

	chunks[0] = strings.TrimLeft(chunks[0], " ")

	return chunks
}

// numberRound takes a number and returns a string containing a rounded to the
// even with the number of decimal places requested.  If this would result in
// the right most decimal place(s) containing "0"s, then all "0"s on the end of
// the decimal portion will be truncated.
func numberRound(number float64, decimals int) string {
	if number == float64(int64(number)) {
		return strconv.FormatInt(int64(number), 10)
	}

	str := fmt.Sprintf("%f", number)
	pos := strings.Index(str, ".")

	if pos != -1 && len(str) > (pos+decimals) {
		str = str[0 : pos+decimals+1]
	}

	backToNum, _ := strconv.ParseFloat(str, 64)
	difference := number - backToNum
	half := 0.5
	for i := 0; i < decimals; i++ {
		half = half / 10
	}

	roundUp := false
	if difference > half {
		roundUp = true
	} else if difference == half {
		// for halfs, round to even
		lastDigit := str[:len(str)-1]
		roundUp = lastDigit == "1" || lastDigit == "3" || lastDigit == "5" || lastDigit == "7" || lastDigit == "9"
	}

	if roundUp {
		// multiply, then ceil, then divide
		multiplier := math.Pow(float64(10), float64(decimals))
		multiplied := strconv.FormatFloat(math.Ceil(number*multiplier), 'f', 0, 64)

		if len(multiplied) > decimals {
			str = multiplied[:len(multiplied)-decimals] + "." + multiplied[len(multiplied)-decimals:]
		} else {
			str = "0." + strings.Repeat("0", decimals-len(multiplied)) + multiplied
		}
	}

	str = strings.TrimRight(str, "0")
	str = strings.TrimRight(str, ".")

	return str
}
