// Copyright 2015, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

// Package unitconv implements string conversion functionality for
// unit prefixes such as those from the SI and IEC standards.
//
// See https://wikipedia.org/wiki/unit_prefix
package unitconv

import (
	"errors"
	"math"
	"strconv"
	"strings"
)

// Mode is the base conversion for strings.
type Mode int

// These are the different modes for Prefix string conversion.
const (
	// AutoParse will parse the input as either the SI, IEC, or regular float
	// notation as accepted by ParseFloat.
	//
	// This is an invalid mode for Append and Format.
	AutoParse Mode = iota

	// SI uses a scaling of 1000x, and uses a single letter symbol to denote
	// the scaling. The prefixes ranges from Yocto (1E-24) to Yotta (1E+24).
	//
	// The output uses SI prefixes, and uses 'k' for Kilo and 'μ' for Micro.
	// The input accepts SI prefixes along with 'k' and 'K' for Kilo and
	// 'μ' and 'u' for Micro.
	// It does not support the Deca, Hecto, Deci, or Centi prefixes.
	SI

	// Base1024 uses a scaling of 1024x, but uses the same prefixes as SI. This
	// is a non-standard prefix notation and exists because many legacy systems
	// unfortunately operate in this way.
	//
	// The output uses SI prefixes, but uses 'K' for Kilo and 'u' for Micro.
	// The input accepts SI prefixes along with 'k' and 'K' for Kilo and
	// 'μ' and 'u' for Micro. It also accepts IEC notation.
	Base1024

	// IEC uses a scaling of 1024x, and uses a two-letter symbol to denote the
	// scaling in a system similar to SI. Instead of Kilo denoted as 'k', it
	// uses Kibi denoted as 'Ki'. The prefixes ranges from Unit (1) to Yobi
	// (1<<80). Representing values less than 1 will not use any of the divisor
	// prefixes. IEC notation is easy to identify since the prefix symbols
	// always end with the letter 'i'.
	//
	// The output uses IEC prefixes that are Unit and greater.
	// The input accepts IEC prefixes only.
	IEC
)

func (m Mode) base() float64 {
	switch m {
	case SI:
		return 1000.0
	case Base1024, IEC:
		return 1024.0
	default:
		return math.NaN()
	}
}

// Prefix factors according to IEC standards.
const (
	// These are not standard IEC prefixes, but used internally.
	yocbi = 1.0 / (1 << 80)
	zepbi = 1.0 / (1 << 70)
	attbi = 1.0 / (1 << 60)
	fembi = 1.0 / (1 << 50)
	picbi = 1.0 / (1 << 40)
	nanbi = 1.0 / (1 << 30)
	micbi = 1.0 / (1 << 20)
	milbi = 1.0 / (1 << 10)

	_ = 1 << 0

	Kibi = 1 << 10
	Mebi = 1 << 20
	Gibi = 1 << 30
	Tebi = 1 << 40
	Pebi = 1 << 50
	Exbi = 1 << 60
	Zebi = 1 << 70
	Yobi = 1 << 80
)

// Prefix factors according to SI standards.
const (
	Yocto = 1E-24
	Zepto = 1E-21
	Atto  = 1E-18
	Femto = 1E-15
	Pico  = 1E-12
	Nano  = 1E-9
	Micro = 1E-6
	Milli = 1E-3

	Unit = 1E0 // Not a standard SI prefix.

	Kilo  = 1E+3
	Mega  = 1E+6
	Giga  = 1E+9
	Tera  = 1E+12
	Peta  = 1E+15
	Exa   = 1E+18
	Zetta = 1E+21
	Yotta = 1E+24
)

const (
	prefixes = divPrefixes + string(unitPrefix) + mulPrefixes

	divPrefixes = "yzafpnum"
	unitPrefix  = '.'
	mulPrefixes = "KMGTPEZY"

	parsePrefixes = divPrefixes + mulPrefixes + string(kiloAlt) + string(microAlt)

	maxExp = +len(mulPrefixes)
	minExp = -len(divPrefixes)

	microNorm, kiloNorm = 'u', 'K'
	microAlt, kiloAlt   = 'μ', 'k'
)

var (
	mapNormToAlt = map[rune]rune{microNorm: microAlt, kiloNorm: kiloAlt}
	mapAltToNorm = map[rune]rune{microAlt: microNorm, kiloAlt: kiloNorm}

	scaleSI = []float64{
		Yocto, Zepto, Atto, Femto, Pico, Nano, Micro, Milli,
		Unit,
		Kilo, Mega, Giga, Tera, Peta, Exa, Zetta, Yotta,
	}
	scaleIEC = []float64{
		yocbi, zepbi, attbi, fembi, picbi, nanbi, micbi, milbi,
		Unit,
		Kibi, Mebi, Gibi, Tebi, Pebi, Exbi, Zebi, Yobi,
	}
)

// Using a combination of the math.LogX and math.Pow functions can be lossy.
// This leads to slightly wrong values around the prefix boundaries. Thus, we
// look up the computed exponent in an authoritative list of scalings and
// adjust accordingly. We only check up to 3 values in the relevant section,
// ensuring a runtime of O(1).
func adjustLog(val float64, scales []float64, minExp, exp, maxExp int) int {
	exp++
	if exp > maxExp {
		exp = maxExp
	}
	for exp >= minExp && scales[exp+len(scales)/2] > math.Abs(val) {
		exp--
	}
	if exp < minExp {
		exp = minExp
	}
	return exp
}

// AppendPrefix appends the string form of the floating-point number val, as
// generated by FormatPrefix, to dst and returns the extended buffer.
func AppendPrefix(dst []byte, val float64, m Mode, prec int) (out []byte) {
	if math.IsNaN(val) || math.IsInf(val, 0) {
		return strconv.AppendFloat(dst, val, 'f', -1, 64)
	}

	// Compute the prefix exponent.
	var exp int
	var adjustValue bool
	if val != 0 {
		switch m {
		case SI:
			exp = int(math.Floor(math.Log10(math.Abs(val)) / 3))
			exp = adjustLog(val, scaleSI, minExp, exp, maxExp)
			val /= scaleSI[exp+len(scaleSI)/2] // Same as: Pow(1000, exp)
			adjustValue = minExp < exp && exp < maxExp
		case Base1024:
			exp = int(math.Floor(math.Log2(math.Abs(val)) / 10))
			exp = adjustLog(val, scaleIEC, minExp, exp, maxExp)
			val /= scaleIEC[exp+len(scaleSI)/2] // Same as: Pow(1024, exp)
			adjustValue = minExp < exp && exp < maxExp
		case IEC:
			exp = int(math.Floor(math.Log2(math.Abs(val)) / 10))
			exp = adjustLog(val, scaleIEC, 0, exp, maxExp)
			val /= scaleIEC[exp+len(scaleSI)/2] // Same as: Pow(1024, exp)
			adjustValue = 0 < exp && exp < maxExp
		default:
			return strconv.AppendInt(append(dst, '%'), int64(m), 10)
		}
	} else {
		exp = 0
	}
	if adjustValue {
		if math.Abs(val) < 1 {
			val = math.Copysign(1, val)
		}
		if math.Abs(val) >= m.base() {
			val = math.Copysign(math.Nextafter(m.base(), 0), val)
		}
	}

	// Print the actual number.
	dst = strconv.AppendFloat(dst, val, 'f', prec, 64)

	// Print the prefix symbol.
	if exp != 0 {
		sym := prefixes[exp+len(prefixes)/2]
		if alt := mapNormToAlt[rune(sym)]; m == SI && alt != 0 {
			dst = append(dst, string(alt)...)
		} else {
			dst = append(dst, sym)
		}
		if m == IEC {
			dst = append(dst, 'i')
		}
	}
	return dst
}

// FormatPrefix converts the floating-point number val to a string, according
// to the prefix notation specified by mode. The prec specifies the precision
// used by the numeric portion.
//
// Even if prec is -1, formatting a value and parsing it does not guarantee that
// the exact value will be returned. It will however be extremely accurate.
//
// It is valid to format +Inf, -Inf, and NaN.
func FormatPrefix(val float64, m Mode, prec int) (str string) {
	return string(AppendPrefix(nil, val, m, prec))
}

// ParsePrefix converts the string str to a floating-point number, according to
// the prefix notation specified by mode.
//
// It is valid to parse +Inf, -Inf, and NaN.
func ParsePrefix(str string, m Mode) (val float64, err error) {
	val, err = strconv.ParseFloat(str, 64)
	if err == nil && (m == AutoParse || math.IsNaN(val) || math.IsInf(val, 0)) {
		return val, nil
	}
	err = nil // Reset the error

	// If mode is AutoParse, detect the format to use.
	if m == AutoParse {
		if len(str) > 0 && str[len(str)-1] == 'i' {
			m = IEC
		} else {
			m = SI
		}
	}

	// Parse the prefix symbol.
	var exp int
	var saveStr = str
	if i := strings.IndexAny(str, parsePrefixes); i >= 0 {
		strPre := str[i:]
		str = str[:i]

		if m == IEC && len(strPre) != 2 {
			goto fail
		}
		for si, ch := range strPre {
			switch si {
			case 0:
				norm, usedAlt := mapAltToNorm[ch]
				if usedAlt {
					ch = norm
				}
				exp = strings.IndexByte(prefixes, byte(ch)) - len(prefixes)/2
				if m == IEC && (usedAlt || exp < 0) {
					goto fail
				}
			case 1:
				if m == SI || ch != 'i' {
					goto fail
				}
			default:
				goto fail
			}
		}
	}

	// Parse the number part.
	if strings.Trim(str, "-+.0123456789") != "" {
		goto fail
	}
	if val, err = strconv.ParseFloat(str, 64); err != nil {
		goto fail
	}

	// Compute the actual value.
	switch m {
	case SI:
		return val * scaleSI[exp+len(scaleSI)/2], nil
	case Base1024, IEC:
		return val * scaleIEC[exp+len(scaleSI)/2], nil
	default:
		return 0, errors.New("unitconv: invalid mode")
	}

fail:
	if err == nil {
		err = strconv.ErrSyntax
	}
	if nerr, ok := err.(*strconv.NumError); ok {
		err = nerr.Err
	}
	err = &strconv.NumError{Func: "ParsePrefix", Num: saveStr, Err: err}
	return val, err
}
