//go:build !dnum

package fixedpoint

import (
	"bytes"
	"database/sql/driver"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"sync/atomic"
)

const MaxPrecision = 12
const DefaultPrecision = 8

const DefaultPow = 1e8

type Value int64

const Zero = Value(0)
const One = Value(1e8)
const NegOne = Value(-1e8)
const PosInf = Value(math.MaxInt64)
const NegInf = Value(math.MinInt64)

type RoundingMode int

const (
	Up RoundingMode = iota
	Down
	HalfUp
)

// Trunc returns the integer portion (truncating any fractional part)
func (v Value) Trunc() Value {
	return NewFromFloat(math.Floor(v.Float64()))
}

func (v Value) Round(r int, mode RoundingMode) Value {
	pow := math.Pow10(r)
	f := v.Float64() * pow
	switch mode {
	case Up:
		f = math.Ceil(f) / pow
	case HalfUp:
		f = math.Floor(f+0.5) / pow
	case Down:
		f = math.Floor(f) / pow
	}

	s := strconv.FormatFloat(f, 'f', r, 64)
	return MustNewFromString(s)
}

func (v Value) Value() (driver.Value, error) {
	return v.Float64(), nil
}

func (v *Value) Scan(src interface{}) error {
	switch d := src.(type) {
	case int64:
		*v = NewFromInt(d)
		return nil

	case float64:
		*v = NewFromFloat(d)
		return nil

	case []byte:
		vv, err := NewFromString(string(d))
		if err != nil {
			return err
		}
		*v = vv
		return nil

	default:

	}

	return fmt.Errorf("fixedpoint.Value scan error, type: %T is not supported, value; %+v", src, src)
}

func (v Value) Float64() float64 {
	if v == PosInf {
		return math.Inf(1)
	} else if v == NegInf {
		return math.Inf(-1)
	}
	return float64(v) / DefaultPow
}

func (v Value) Abs() Value {
	if v < 0 {
		return -v
	}
	return v
}

func (v Value) String() string {
	if v == PosInf {
		return "inf"
	} else if v == NegInf {
		return "-inf"
	}
	return strconv.FormatFloat(float64(v)/DefaultPow, 'f', -1, 64)
}

func (v Value) FormatString(prec int) string {
	if v == PosInf {
		return "inf"
	} else if v == NegInf {
		return "-inf"
	}

	u := int64(v)

	// trunc precision
	precDiff := DefaultPrecision - prec
	if precDiff > 0 {
		powDiff := int64(math.Round(math.Pow10(precDiff)))
		u = int64(v) / powDiff * powDiff
	}

	// check sign
	sign := Value(u).Sign()

	basePow := int64(DefaultPow)
	a := u / basePow
	b := u % basePow

	if a < 0 {
		a = -a
	}

	if b < 0 {
		b = -b
	}

	str := strconv.FormatInt(a, 10)
	if prec > 0 {
		bStr := fmt.Sprintf(".%08d", b)
		if prec <= DefaultPrecision {
			bStr = bStr[0 : prec+1]
		} else {
			for i := prec - DefaultPrecision; i > 0; i-- {
				bStr += "0"
			}
		}

		str += bStr
	}

	if sign < 0 {
		str = "-" + str
	}

	return str
}

func (v Value) Percentage() string {
	if v == 0 {
		return "0"
	}
	if v == PosInf {
		return "inf%"
	} else if v == NegInf {
		return "-inf%"
	}

	trcv := math.Trunc(float64(v)/DefaultPow*100.*1e4) / 1e4
	return strconv.FormatFloat(trcv, 'f', -1, 64) + "%"
}

func (v Value) FormatPercentage(prec int) string {
	if v == 0 {
		return "0"
	}
	if v == PosInf {
		return "inf%"
	} else if v == NegInf {
		return "-inf%"
	}
	pow := math.Pow10(prec)
	result := strconv.FormatFloat(
		math.Trunc(float64(v)/DefaultPow*pow*100.)/pow, 'f', prec, 64)
	return result + "%"
}

func (v Value) SignedPercentage() string {
	if v > 0 {
		return "+" + v.Percentage()
	}
	return v.Percentage()
}

func (v Value) Int64() int64 {
	return int64(v.Float64())
}

func (v Value) Int() int {
	n := v.Int64()
	if int64(int(n)) != n {
		panic("unable to convert Value to int32")
	}
	return int(n)
}

func (v Value) Neg() Value {
	return -v
}

// TODO inf
func (v Value) Sign() int {
	if v > 0 {
		return 1
	} else if v == 0 {
		return 0
	} else {
		return -1
	}
}

func (v Value) IsZero() bool {
	return v == 0
}

func Mul(x, y Value) Value {
	return NewFromFloat(x.Float64() * y.Float64())
}

func (v Value) Mul(v2 Value) Value {
	return NewFromFloat(v.Float64() * v2.Float64())
}

func Div(x, y Value) Value {
	return NewFromFloat(x.Float64() / y.Float64())
}

func (v Value) Div(v2 Value) Value {
	return NewFromFloat(v.Float64() / v2.Float64())
}

func (v Value) Floor() Value {
	return NewFromFloat(math.Floor(v.Float64()))
}

func (v Value) Ceil() Value {
	return NewFromFloat(math.Ceil(v.Float64()))
}

func (v Value) Sub(v2 Value) Value {
	return Value(int64(v) - int64(v2))
}

func (v Value) Add(v2 Value) Value {
	return Value(int64(v) + int64(v2))
}

func (v *Value) AtomicAdd(v2 Value) {
	atomic.AddInt64((*int64)(v), int64(v2))
}

func (v *Value) AtomicLoad() Value {
	i := atomic.LoadInt64((*int64)(v))
	return Value(i)
}

func (v *Value) UnmarshalYAML(unmarshal func(a interface{}) error) (err error) {
	var s string
	if err = unmarshal(&s); err == nil {
		nv, err2 := NewFromString(s)
		if err2 == nil {
			*v = nv
			return
		}
	}

	return err
}

func (v Value) MarshalYAML() (interface{}, error) {
	return v.FormatString(DefaultPrecision), nil
}

func (v Value) MarshalJSON() ([]byte, error) {
	if v.IsInf() {
		return []byte("\"" + v.String() + "\""), nil
	}
	return []byte(v.FormatString(DefaultPrecision)), nil
}

func (v *Value) UnmarshalJSON(data []byte) error {
	if bytes.Equal(data, []byte{'n', 'u', 'l', 'l'}) {
		*v = Zero
		return nil
	}
	if len(data) == 0 || bytes.Equal(data, []byte{'"', '"'}) {
		*v = Zero
		return nil
	}
	var err error
	if data[0] == '"' {
		data = data[1 : len(data)-1]
	}
	if *v, err = NewFromString(string(data)); err != nil {
		return err
	}
	return nil
}

var ErrPrecisionLoss = errors.New("precision loss")

func Parse(input string) (num int64, numDecimalPoints int, err error) {
	length := len(input)
	isPercentage := input[length-1] == '%'
	if isPercentage {
		length -= 1
		input = input[0:length]
	}

	var neg int64 = 1
	var digit int64
	for i := 0; i < length; i++ {
		c := input[i]
		if c == '-' {
			neg = -1
		} else if c >= '0' && c <= '9' {
			digit, err = strconv.ParseInt(string(c), 10, 64)
			if err != nil {
				return
			}

			num = num*10 + digit
		} else if c == '.' {
			i++
			if i > len(input)-1 {
				err = fmt.Errorf("expect fraction numbers after dot")
				return
			}

			for j := i; j < len(input); j++ {
				fc := input[j]
				if fc >= '0' && fc <= '9' {
					digit, err = strconv.ParseInt(string(fc), 10, 64)
					if err != nil {
						return
					}

					numDecimalPoints++
					num = num*10 + digit

					if numDecimalPoints >= MaxPrecision {
						return num, numDecimalPoints, ErrPrecisionLoss
					}
				} else {
					err = fmt.Errorf("expect digit, got %c", fc)
					return
				}
			}
			break
		} else {
			err = fmt.Errorf("unexpected char %c", c)
			return
		}
	}

	num = num * neg
	if isPercentage {
		numDecimalPoints += 2
	}

	return num, numDecimalPoints, nil
}

func NewFromString(input string) (Value, error) {
	length := len(input)

	if length == 0 {
		return 0, nil
	}

	isPercentage := input[length-1] == '%'
	if isPercentage {
		input = input[0 : length-1]
	}
	dotIndex := -1
	hasDecimal := false
	decimalCount := 0
	// if is decimal, we don't need this
	hasScientificNotion := false
	hasIChar := false
	scIndex := -1
	for i, c := range input {
		if hasDecimal {
			if c <= '9' && c >= '0' {
				decimalCount++
			} else {
				break
			}

		} else if c == '.' {
			dotIndex = i
			hasDecimal = true
		}
		if c == 'e' || c == 'E' {
			hasScientificNotion = true
			scIndex = i
			break
		}
		if c == 'i' || c == 'I' {
			hasIChar = true
			break
		}
	}
	if hasDecimal {
		after := input[dotIndex+1:]
		if decimalCount >= 8 {
			after = after[0:8] + "." + after[8:]
		} else {
			after = after[0:decimalCount] + strings.Repeat("0", 8-decimalCount) + after[decimalCount:]
		}
		input = input[0:dotIndex] + after
		v, err := strconv.ParseFloat(input, 64)
		if err != nil {
			return 0, err
		}

		if isPercentage {
			v = v * 0.01
		}

		return Value(int64(math.Trunc(v))), nil

	} else if hasScientificNotion {
		exp, err := strconv.ParseInt(input[scIndex+1:], 10, 32)
		if err != nil {
			return 0, err
		}
		v, err := strconv.ParseFloat(input[0:scIndex+1]+strconv.FormatInt(exp+8, 10), 64)
		if err != nil {
			return 0, err
		}
		return Value(int64(math.Trunc(v))), nil
	} else if hasIChar {
		if floatV, err := strconv.ParseFloat(input, 64); nil != err {
			return 0, err
		} else if math.IsInf(floatV, 1) {
			return PosInf, nil
		} else if math.IsInf(floatV, -1) {
			return NegInf, nil
		} else {
			return 0, fmt.Errorf("fixedpoint.Value parse error, invalid input string %s", input)
		}
	} else {
		v, err := strconv.ParseInt(input, 10, 64)
		if err != nil {
			return 0, err
		}
		if isPercentage {
			v = v * DefaultPow / 100
		} else {
			v = v * DefaultPow
		}
		return Value(v), nil
	}
}

func MustNewFromString(input string) Value {
	v, err := NewFromString(input)
	if err != nil {
		panic(fmt.Errorf("can not parse %s into fixedpoint, error: %s", input, err.Error()))
	}
	return v
}

func NewFromBytes(input []byte) (Value, error) {
	return NewFromString(string(input))
}

func MustNewFromBytes(input []byte) (v Value) {
	var err error
	if v, err = NewFromString(string(input)); err != nil {
		return Zero
	}
	return v
}

func Must(v Value, err error) Value {
	if err != nil {
		panic(err)
	}
	return v
}

func NewFromFloat(val float64) Value {
	if math.IsInf(val, 1) {
		return PosInf
	} else if math.IsInf(val, -1) {
		return NegInf
	}
	return Value(int64(math.Trunc(val * DefaultPow)))
}

func NewFromInt(val int64) Value {
	return Value(val * DefaultPow)
}

func (a Value) IsInf() bool {
	return a == PosInf || a == NegInf
}

func (a Value) MulExp(exp int) Value {
	return Value(int64(float64(a) * math.Pow(10, float64(exp))))
}

func (a Value) NumIntDigits() int {
	digits := 0
	target := int64(a)
	for pow := int64(DefaultPow); pow <= target; pow *= 10 {
		digits++
	}
	return digits
}

// TODO: speedup
func (a Value) NumFractionalDigits() int {
	if a == 0 {
		return 0
	}
	numPow := 0
	for pow := int64(DefaultPow); pow%10 != 1; pow /= 10 {
		numPow++
	}
	numZeros := 0
	for v := int64(a); v%10 == 0; v /= 10 {
		numZeros++
	}
	return numPow - numZeros
}

func Compare(x, y Value) int {
	if x > y {
		return 1
	} else if x == y {
		return 0
	} else {
		return -1
	}
}

func (x Value) Compare(y Value) int {
	if x > y {
		return 1
	} else if x == y {
		return 0
	} else {
		return -1
	}
}

func Min(a, b Value) Value {
	if a.Compare(b) < 0 {
		return a
	}

	return b
}

func Max(a, b Value) Value {
	if a.Compare(b) > 0 {
		return a
	}

	return b
}

func Equal(x, y Value) bool {
	return x == y
}

func (x Value) Eq(y Value) bool {
	return x == y
}

func Abs(a Value) Value {
	if a < 0 {
		return -a
	}
	return a
}

func Clamp(x, min, max Value) Value {
	if x < min {
		return min
	}
	if x > max {
		return max
	}
	return x
}

func (x Value) Clamp(min, max Value) Value {
	if x < min {
		return min
	}
	if x > max {
		return max
	}
	return x
}
