// Copyright IBM Corp. 2015, 2025
// SPDX-License-Identifier: MPL-2.0

package structs

import (
	"fmt"
	"math/big"
	"strconv"
	"strings"
	"unicode"

	"github.com/hashicorp/nomad/helper/pointer"
)

const (
	// floatPrecision is the precision used before rounding. It is set to a high
	// number to give a high chance of correctly returning equality.
	floatPrecision = uint(256)
)

// BaseUnit is a unique base unit. All units that share the same base unit
// should be comparable.
type BaseUnit uint16

const (
	UnitScalar BaseUnit = iota
	UnitByte
	UnitByteRate
	UnitHertz
	UnitWatt
)

// Unit describes a unit and its multiplier over the base unit type
type Unit struct {
	// Name is the name of the unit (GiB, MB/s)
	Name string

	// Base is the base unit for the unit
	Base BaseUnit

	// Multiplier is the multiplier over the base unit (KiB multiplier is 1024)
	Multiplier int64

	// InverseMultiplier specifies that the multiplier is an inverse so:
	// Base / Multiplier. For example a mW is a W/1000.
	InverseMultiplier bool
}

// Comparable returns if two units are comparable
func (u *Unit) Comparable(o *Unit) bool {
	if u == nil || o == nil {
		return false
	}

	return u.Base == o.Base
}

// ParseAttribute takes a string and parses it into an attribute, pulling out
// units if they are specified as a suffix on a number.
func ParseAttribute(input string) *Attribute {
	ll := len(input)
	if ll == 0 {
		return &Attribute{String: pointer.Of(input)}
	}

	// Check if the string is a number ending with potential units
	var unit string
	numeric := input
	if unicode.IsLetter(rune(input[ll-1])) {
		// Try suffix matching
		for _, u := range lengthSortedUnits {
			if strings.HasSuffix(input, u) {
				unit = u
				break
			}
		}

		// Check if we know about the unit.
		if len(unit) != 0 {
			numeric = strings.TrimSpace(strings.TrimSuffix(input, unit))
		}
	}

	// Try to parse as an int
	i, err := strconv.ParseInt(numeric, 10, 64)
	if err == nil {
		return &Attribute{Int: pointer.Of(i), Unit: unit}
	}

	// Try to parse as a float
	f, err := strconv.ParseFloat(numeric, 64)
	if err == nil {
		return &Attribute{Float: pointer.Of(f), Unit: unit}
	}

	// Try to parse as a bool
	b, err := strconv.ParseBool(input)
	if err == nil {
		return &Attribute{Bool: pointer.Of(b)}
	}

	return &Attribute{String: pointer.Of(input)}
}

// Attribute is used to describe the value of an attribute, optionally
// specifying units
type Attribute struct {
	// Float is the float value for the attribute
	Float *float64

	// Int is the int value for the attribute
	Int *int64

	// String is the string value for the attribute
	String *string

	// Bool is the bool value for the attribute
	Bool *bool

	// Unit is the optional unit for the set int or float value
	Unit string
}

// NewStringAttribute returns a new string attribute.
func NewStringAttribute(s string) *Attribute {
	return &Attribute{
		String: pointer.Of(s),
	}
}

// NewBoolAttribute returns a new boolean attribute.
func NewBoolAttribute(b bool) *Attribute {
	return &Attribute{
		Bool: pointer.Of(b),
	}
}

// NewIntAttribute returns a new integer attribute. The unit is not checked
// to be valid.
func NewIntAttribute(i int64, unit string) *Attribute {
	return &Attribute{
		Int:  pointer.Of(i),
		Unit: unit,
	}
}

// NewFloatAttribute returns a new float attribute. The unit is not checked to
// be valid.
func NewFloatAttribute(f float64, unit string) *Attribute {
	return &Attribute{
		Float: pointer.Of(f),
		Unit:  unit,
	}
}

// GetString returns the string value of the attribute or false if the attribute
// doesn't contain a string.
func (a *Attribute) GetString() (value string, ok bool) {
	if a.String == nil {
		return "", false
	}

	return *a.String, true
}

// GetBool returns the boolean value of the attribute or false if the attribute
// doesn't contain a boolean.
func (a *Attribute) GetBool() (value bool, ok bool) {
	if a.Bool == nil {
		return false, false
	}

	return *a.Bool, true
}

// GetInt returns the integer value of the attribute or false if the attribute
// doesn't contain a integer.
func (a *Attribute) GetInt() (value int64, ok bool) {
	if a.Int == nil {
		return 0, false
	}

	return *a.Int, true
}

// GetFloat returns the float value of the attribute or false if the attribute
// doesn't contain a float.
func (a *Attribute) GetFloat() (value float64, ok bool) {
	if a.Float == nil {
		return 0.0, false
	}

	return *a.Float, true
}

// Copy returns a copied version of the attribute
func (a *Attribute) Copy() *Attribute {
	if a == nil {
		return nil
	}

	ca := &Attribute{
		Unit: a.Unit,
	}

	if a.Float != nil {
		ca.Float = pointer.Of(*a.Float)
	}
	if a.Int != nil {
		ca.Int = pointer.Of(*a.Int)
	}
	if a.Bool != nil {
		ca.Bool = pointer.Of(*a.Bool)
	}
	if a.String != nil {
		ca.String = pointer.Of(*a.String)
	}

	return ca
}

// GoString returns a string representation of the attribute
func (a *Attribute) GoString() string {
	if a == nil {
		return "nil attribute"
	}

	var b strings.Builder
	if a.Float != nil {
		b.WriteString(fmt.Sprintf("%v", *a.Float))
	} else if a.Int != nil {
		b.WriteString(fmt.Sprintf("%v", *a.Int))
	} else if a.Bool != nil {
		b.WriteString(fmt.Sprintf("%v", *a.Bool))
	} else if a.String != nil {
		b.WriteString(*a.String)
	}

	if a.Unit != "" {
		b.WriteString(a.Unit)
	}

	return b.String()
}

// Validate checks if the attribute is valid
func (a *Attribute) Validate() error {
	if a.Unit != "" {
		if _, ok := UnitIndex[a.Unit]; !ok {
			return fmt.Errorf("unrecognized unit %q", a.Unit)
		}

		// Check only int/float set
		if a.String != nil || a.Bool != nil {
			return fmt.Errorf("unit can not be specified on a boolean or string attribute")
		}
	}

	// Assert only one of the attributes is set
	set := 0
	if a.Float != nil {
		set++
	}
	if a.Int != nil {
		set++
	}
	if a.String != nil {
		set++
	}
	if a.Bool != nil {
		set++
	}

	if set == 0 {
		return fmt.Errorf("no attribute value set")
	} else if set > 1 {
		return fmt.Errorf("only one attribute value may be set")
	}

	return nil
}

// Comparable returns whether the two attributes are comparable
func (a *Attribute) Comparable(b *Attribute) bool {
	if a == nil || b == nil {
		return false
	}

	// First use the units to decide if comparison is possible
	aUnit := a.getTypedUnit()
	bUnit := b.getTypedUnit()
	if aUnit != nil && bUnit != nil {
		return aUnit.Comparable(bUnit)
	} else if aUnit != nil && bUnit == nil {
		return false
	} else if aUnit == nil && bUnit != nil {
		return false
	}

	if a.String != nil {
		return b.String != nil
	}

	if a.Bool != nil {
		return b.Bool != nil
	}

	return true
}

// Compare compares two attributes. If the returned boolean value is false, it
// means the values are not comparable, either because they are of different
// types (bool versus int) or the units are incompatible for comparison.
// The returned int will be 0 if a==b, -1 if a < b, and +1 if a > b for all
// values but bool. For bool it will be 0 if a==b or 1 if a!=b.
func (a *Attribute) Compare(b *Attribute) (int, bool) {
	if !a.Comparable(b) {
		return 0, false
	}

	return a.comparator()(b)
}

// Equal returns true if two Attributes are comparable and have equal values.
func (a *Attribute) Equal(b *Attribute) bool {
	val, isComparable := a.Compare(b)
	return isComparable && val == 0
}

// comparator returns the comparator function for the attribute
func (a *Attribute) comparator() compareFn {
	if a.Bool != nil {
		return a.boolComparator
	}
	if a.String != nil {
		return a.stringComparator
	}
	if a.Int != nil || a.Float != nil {
		return a.numberComparator
	}

	return nullComparator
}

// boolComparator compares two boolean attributes
func (a *Attribute) boolComparator(b *Attribute) (int, bool) {
	if *a.Bool == *b.Bool {
		return 0, true
	}

	return 1, true
}

// stringComparator compares two string attributes
func (a *Attribute) stringComparator(b *Attribute) (int, bool) {
	return strings.Compare(*a.String, *b.String), true
}

// numberComparator compares two number attributes, having either Int or Float
// set.
func (a *Attribute) numberComparator(b *Attribute) (int, bool) {
	// If they are both integers we do perfect precision comparisons
	if a.Int != nil && b.Int != nil {
		return a.intComparator(b)
	}

	// Push both into the float space
	af := a.getBigFloat()
	bf := b.getBigFloat()
	if af == nil || bf == nil {
		return 0, false
	}

	return af.Cmp(bf), true
}

// intComparator compares two integer attributes.
func (a *Attribute) intComparator(b *Attribute) (int, bool) {
	ai := a.getInt()
	bi := b.getInt()

	if ai == bi {
		return 0, true
	} else if ai < bi {
		return -1, true
	} else {
		return 1, true
	}
}

// nullComparator always returns false and is used when no comparison function
// is possible
func nullComparator(*Attribute) (int, bool) {
	return 0, false
}

// compareFn is used to compare two attributes. It returns -1, 0, 1 for ordering
// and a boolean for if the comparison is possible.
type compareFn func(b *Attribute) (int, bool)

// getBigFloat returns a big.Float representation of the attribute, converting
// the value to the base unit if a unit is specified.
func (a *Attribute) getBigFloat() *big.Float {
	f := new(big.Float)
	f.SetPrec(floatPrecision)
	if a.Int != nil {
		f.SetInt64(*a.Int)
	} else if a.Float != nil {
		f.SetFloat64(*a.Float)
	} else {
		return nil
	}

	// Get the unit
	u := a.getTypedUnit()

	// If there is no unit just return the float
	if u == nil {
		return f
	}

	// Convert to the base unit
	multiplier := new(big.Float)
	multiplier.SetPrec(floatPrecision)
	multiplier.SetInt64(u.Multiplier)
	if u.InverseMultiplier {
		base := big.NewFloat(1.0)
		base.SetPrec(floatPrecision)
		multiplier = multiplier.Quo(base, multiplier)
	}

	f.Mul(f, multiplier)
	return f
}

// getInt returns an int representation of the attribute, converting
// the value to the base unit if a unit is specified.
func (a *Attribute) getInt() int64 {
	if a.Int == nil {
		return 0
	}

	i := *a.Int

	// Get the unit
	u := a.getTypedUnit()

	// If there is no unit just return the int
	if u == nil {
		return i
	}

	if u.InverseMultiplier {
		i /= u.Multiplier
	} else {
		i *= u.Multiplier
	}

	return i
}

// getTypedUnit returns the Unit for the attribute or nil if no unit exists.
func (a *Attribute) getTypedUnit() *Unit {
	return UnitIndex[a.Unit]
}
