// Copyright (C) MongoDB, Inc. 2014-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package util

import (
	"fmt"
	"reflect"
)

// Numeric Conversion Tools

type converterFunc func(interface{}) (interface{}, error)

// this helper makes it simple to generate new numeric converters,
// be sure to assign them on a package level instead of dynamically
// within a function to avoid low performance.
func newNumberConverter(targetType reflect.Type) converterFunc {
	return func(number interface{}) (interface{}, error) {
		// to avoid panics on nil values
		if number == nil {
			return nil, fmt.Errorf("cannot convert nil value")
		}
		v := reflect.ValueOf(number)
		if !v.Type().ConvertibleTo(targetType) {
			return nil, fmt.Errorf("cannot convert %v to %v", v.Type(), targetType)
		}
		converted := v.Convert(targetType)
		return converted.Interface(), nil
	}
}

// making this package level so it is only evaluated once.
var uint32Converter = newNumberConverter(reflect.TypeOf(uint32(0)))

// ToUInt32 is a function for converting any numeric type
// into a uint32. This can easily result in a loss of information
// due to truncation, so be careful.
func ToUInt32(number interface{}) (uint32, error) {
	asInterface, err := uint32Converter(number)
	if err != nil {
		return 0, err
	}

	return convert[uint32](asInterface)
}

var intConverter = newNumberConverter(reflect.TypeOf(int(0)))

// ToInt is a function for converting any numeric type
// into an int. This can easily result in a loss of information
// due to truncation of floats.
func ToInt(number interface{}) (int, error) {
	asInterface, err := intConverter(number)
	if err != nil {
		return 0, err
	}

	return convert[int](asInterface)
}

var float64Converter = newNumberConverter(reflect.TypeOf(float64(0)))

// ToFloat64 is a function for converting any numeric type
// into a float64.
func ToFloat64(number interface{}) (float64, error) {
	asInterface, err := float64Converter(number)
	if err != nil {
		return 0, err
	}

	return convert[float64](asInterface)
}

func convert[T any](val any) (T, error) {
	converted, ok := val.(T)
	if !ok {
		return *new(T), fmt.Errorf("Expected %+v (%T) to be float64", val, val)
	}

	return converted, nil
}
