package quantity

import "base:intrinsics"

// ---------------------------------------------------------------------------------------------------------------------
// ----- Constants ------------------------
// ---------------------------------------------------------------------------------------------------------------------
@(private = "file")
kelvins_celsius_offset :: #force_inline proc "contextless" (
	$V: typeid,
) -> V where intrinsics.type_is_numeric(V) {
	when intrinsics.type_is_float(V) {
		OFFSET :: 273.15
	} else {
		OFFSET :: 273
	}
	return OFFSET
}

@(private = "file")
FAHRENHEIT_PER_CELSIUS_DEGREE :: 9.0 / 5.0
@(private = "file")
CELSIUS_PER_FAHRENHEIT_DEGREE :: 5.0 / 9.0

// ---------------------------------------------------------------------------------------------------------------------
// ----- Types ------------------------
// ---------------------------------------------------------------------------------------------------------------------
//----- Kelvins ----------------------------------
Kelvins :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
	v: V,
}

@(private = "file")
kelvins_to_celsius :: #force_inline proc "contextless" (
	kelvins: Kelvins($V),
) -> Celsius(V) where intrinsics.type_is_numeric(V) {
	return Celsius(V){kelvins.v - kelvins_celsius_offset(V)}
}

@(private = "file")
kelvins_to_deci_kelvins :: #force_inline proc "contextless" (
	kelvins: Kelvins($V),
) -> Deci_Kelvins(V) where intrinsics.type_is_numeric(V) {
	return Deci_Kelvins(V){kelvins.v * DECI}
}

@(private = "file")
kelvins_to_fahrenheit :: #force_inline proc "contextless" (
	kelvins: Kelvins($V),
) -> Fahrenheit(V) where intrinsics.type_is_numeric(V) {
	return celsius_to_fahrenheit(kelvins_to_celsius(kelvins))
}

//----- Decikelvins ----------------------------------
Deci_Kelvins :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
	v: V,
}

@(private = "file")
deci_kelvins_to_kelvins :: #force_inline proc "contextless" (
	deci_kelvins: Deci_Kelvins($V),
) -> Kelvins(V) where intrinsics.type_is_numeric(V) {
	return Kelvins(V){deci_kelvins.v / DECI}
}

//----- Degrees Celsius ----------------------------------
Celsius :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
	v: V,
}

@(private = "file")
celsius_to_kelvins :: #force_inline proc "contextless" (
	degrees_celsius: Celsius($V),
) -> Kelvins(V) where intrinsics.type_is_numeric(V) {
	return Kelvins(V){degrees_celsius.v + kelvins_celsius_offset(V)}
}

@(private = "file")
celsius_to_deci_celsius :: #force_inline proc "contextless" (
	degrees_celsius: Celsius($V),
) -> Deci_Celsius(V) where intrinsics.type_is_numeric(V) {
	return Deci_Celsius(V){degrees_celsius.v * DECI}
}

@(private = "file", fast_math = {.Allow_Contract})
celsius_to_fahrenheit :: #force_inline proc "contextless" (
	degrees_celsius: Celsius($V),
) -> Fahrenheit(V) where intrinsics.type_is_numeric(V) {
	when intrinsics.type_is_float(V) {
		return Fahrenheit(V){degrees_celsius.v * FAHRENHEIT_PER_CELSIUS_DEGREE + 32.0}
	} else {
		return Fahrenheit(V){degrees_celsius.v * 9 / 5 + 32}
	}
}

//----- Deci Degrees Celsius ----------------------------------
Deci_Celsius :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
	v: V,
}

@(private = "file")
deci_celsius_to_celsius :: #force_inline proc "contextless" (
	deci_degrees_celsius: Deci_Celsius($V),
) -> Celsius(V) where intrinsics.type_is_numeric(V) {
	return Celsius(V){deci_degrees_celsius.v / DECI}
}

//----- Degrees Fahrenheit ----------------------------------
Fahrenheit :: struct($V: typeid) where intrinsics.type_is_numeric(V) {
	v: V,
}

@(private = "file")
fahrenheit_to_celsius :: #force_inline proc "contextless" (
	degrees_fahrenheit: Fahrenheit($V),
) -> Celsius(V) where intrinsics.type_is_numeric(V) {
	when intrinsics.type_is_float(V) {
		return Celsius(V){(degrees_fahrenheit.v - 32.0) * CELSIUS_PER_FAHRENHEIT_DEGREE}
	} else {
		return Celsius(V){(degrees_fahrenheit.v - 32) * 5 / 9}
	}
}

@(private = "file")
fahrenheit_to_kelvins :: #force_inline proc "contextless" (
	degrees_fahrenheit: Fahrenheit($V),
) -> Kelvins(V) where intrinsics.type_is_numeric(V) {
	return celsius_to_kelvins(fahrenheit_to_celsius(degrees_fahrenheit))
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Conversion Overloads ------------------------
// ---------------------------------------------------------------------------------------------------------------------
to_kelvins :: proc {
	deci_kelvins_to_kelvins,
	celsius_to_kelvins,
	fahrenheit_to_kelvins,
}

to_deci_kelvins :: proc {
	kelvins_to_deci_kelvins,
}

to_celsius :: proc {
	kelvins_to_celsius,
	deci_celsius_to_celsius,
	fahrenheit_to_celsius,
}

to_deci_celsius :: proc {
	celsius_to_deci_celsius,
}

to_fahrenheit :: proc {
	celsius_to_fahrenheit,
	kelvins_to_fahrenheit,
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Tests ------------------------
// ---------------------------------------------------------------------------------------------------------------------
import "core:testing"

@(test)
test_kelvins_to_celsius :: proc(t: ^testing.T) {
	kelvins := Kelvins(f32){273.15}
	celsius := to_celsius(kelvins)

	testing.expect_value(t, celsius, Celsius(f32){0})
}

@(test)
test_kelvins_to_deci_kelvins :: proc(t: ^testing.T) {
	kelvins := Kelvins(int){100}
	deci_kelvins := to_deci_kelvins(kelvins)

	testing.expect_value(t, deci_kelvins, Deci_Kelvins(int){1000})
}

@(test)
test_deci_kelvins_to_kelvins :: proc(t: ^testing.T) {
	deci_kelvins := Deci_Kelvins(int){1000}
	kelvins := to_kelvins(deci_kelvins)

	testing.expect_value(t, kelvins, Kelvins(int){100})
}

@(test)
test_celsius_to_kelvins :: proc(t: ^testing.T) {
	degrees_celsius := Celsius(f32){0}
	kelvins := to_kelvins(degrees_celsius)

	testing.expect_value(t, kelvins, Kelvins(f32){273.15})
}

@(test)
test_celsius_to_deci_celsius :: proc(t: ^testing.T) {
	degrees_celsius := Celsius(int){100}
	deci_degrees_celsius := to_deci_celsius(degrees_celsius)

	testing.expect_value(t, deci_degrees_celsius, Deci_Celsius(int){1000})
}

@(test)
test_deci_celsius_to_celsius :: proc(t: ^testing.T) {
	deci_degrees_celsius := Deci_Celsius(int){1000}
	degrees_celsius := to_celsius(deci_degrees_celsius)

	testing.expect_value(t, degrees_celsius, Celsius(int){100})
}

@(test)
test_celsius_to_fahrenheit :: proc(t: ^testing.T) {
	degrees_celsius := Celsius(int){100}
	degrees_fahrenheit := to_fahrenheit(degrees_celsius)

	testing.expect_value(t, degrees_fahrenheit, Fahrenheit(int){212})
}

@(test)
test_fahrenheit_to_celsius :: proc(t: ^testing.T) {
	degrees_fahrenheit := Fahrenheit(int){212}
	degrees_celsius := to_celsius(degrees_fahrenheit)

	testing.expect_value(t, degrees_celsius, Celsius(int){100})
}

@(test)
test_kelvins_to_fahrenheit :: proc(t: ^testing.T) {
	kelvins := Kelvins(int){373}
	degrees_fahrenheit := to_fahrenheit(kelvins)

	testing.expect_value(t, degrees_fahrenheit, Fahrenheit(int){212})
}

@(test)
test_fahrenheit_to_kelvins :: proc(t: ^testing.T) {
	degrees_fahrenheit := Fahrenheit(int){212}
	kelvins := to_kelvins(degrees_fahrenheit)

	testing.expect_value(t, kelvins, Kelvins(int){373})
}

@(test)
test_celsius_to_fahrenheit_f64 :: proc(t: ^testing.T) {
	// -40 is the point where the Celsius and Fahrenheit scales coincide. It converts exactly in
	// f64, so a passing equality here also confirms the 9/5 ratio constant is not lossy.
	degrees_fahrenheit := to_fahrenheit(Celsius(f64){-40})

	testing.expect_value(t, degrees_fahrenheit, Fahrenheit(f64){-40})
}

@(test)
test_fahrenheit_to_celsius_f64 :: proc(t: ^testing.T) {
	degrees_celsius := to_celsius(Fahrenheit(f64){-40})

	testing.expect_value(t, degrees_celsius, Celsius(f64){-40})
}
