package humanize

import (
	"fmt"
	"math"
	"strconv"
	"strings"
	"unicode"
)

// IEC Sizes.
// kibis of bits
const (
	Byte = 1 << (iota * 10)
	KiByte
	MiByte
	GiByte
	TiByte
	PiByte
	EiByte
)

// SI Sizes.
const (
	IByte = 1
	KByte = IByte * 1000
	MByte = KByte * 1000
	GByte = MByte * 1000
	TByte = GByte * 1000
	PByte = TByte * 1000
	EByte = PByte * 1000
)

var defaultSizeTable = map[string]uint64{
	"b":   Byte,
	"kib": KiByte,
	"kb":  KByte,
	"mib": MiByte,
	"mb":  MByte,
	"gib": GiByte,
	"gb":  GByte,
	"tib": TiByte,
	"tb":  TByte,
	"pib": PiByte,
	"pb":  PByte,
	"eib": EiByte,
	"eb":  EByte,
	// Without suffix
	"":   Byte,
	"ki": KiByte,
	"k":  KByte,
	"mi": MiByte,
	"m":  MByte,
	"gi": GiByte,
	"g":  GByte,
	"ti": TiByte,
	"t":  TByte,
	"pi": PiByte,
	"p":  PByte,
	"ei": EiByte,
	"e":  EByte,
}

var memorysSizeTable = map[string]uint64{
	"b":  Byte,
	"kb": KiByte,
	"mb": MiByte,
	"gb": GiByte,
	"tb": TiByte,
	"pb": PiByte,
	"eb": EiByte,
	"":   Byte,
	"k":  KiByte,
	"m":  MiByte,
	"g":  GiByte,
	"t":  TiByte,
	"p":  PiByte,
	"e":  EiByte,
}

var (
	defaultSizes = []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
	iSizes       = []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
)

func Bytes(s uint64) string {
	return humanateBytes(s, 1000, defaultSizes)
}

func MemoryBytes(s uint64) string {
	return humanateBytes(s, 1024, defaultSizes)
}

func IBytes(s uint64) string {
	return humanateBytes(s, 1024, iSizes)
}

func logn(n, b float64) float64 {
	return math.Log(n) / math.Log(b)
}

func humanateBytes(s uint64, base float64, sizes []string) string {
	if s < 10 {
		return fmt.Sprintf("%d B", s)
	}
	e := math.Floor(logn(float64(s), base))
	suffix := sizes[int(e)]
	val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
	f := "%.0f %s"
	if val < 10 {
		f = "%.1f %s"
	}

	return fmt.Sprintf(f, val, suffix)
}

func ParseBytes(s string) (uint64, error) {
	return parseBytes0(s, defaultSizeTable)
}

func ParseMemoryBytes(s string) (uint64, error) {
	return parseBytes0(s, memorysSizeTable)
}

func parseBytes0(s string, sizeTable map[string]uint64) (uint64, error) {
	lastDigit := 0
	hasComma := false
	for _, r := range s {
		if !(unicode.IsDigit(r) || r == '.' || r == ',') {
			break
		}
		if r == ',' {
			hasComma = true
		}
		lastDigit++
	}

	num := s[:lastDigit]
	if hasComma {
		num = strings.Replace(num, ",", "", -1)
	}

	f, err := strconv.ParseFloat(num, 64)
	if err != nil {
		return 0, err
	}

	extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
	if m, ok := sizeTable[extra]; ok {
		f *= float64(m)
		if f >= math.MaxUint64 {
			return 0, fmt.Errorf("too large: %v", s)
		}
		return uint64(f), nil
	}

	return 0, fmt.Errorf("unhandled size name: %v", extra)
}
