--[[----------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2025 Adobe
All Rights Reserved.

NOTICE: All information contained herein is, and remains
the property of Adobe and its suppliers, if any. The intellectual
and technical concepts contained herein are proprietary to Adobe
and its suppliers and are protected by all applicable intellectual
property laws, including trade secret and copyright laws.
Dissemination of this information or reproduction of this material
is strictly forbidden unless prior written permission is obtained
from Adobe.

------------------------------------------------------------------------------]]

--============================================================================--

local WFMathUtils = {}

---------------------------------------------------------------------------------
-- @mark Math Utils
---------------------------------------------------------------------------------

--[[!
	\brief Calculates the standard deviation of a set of values.

	\param values (table) A table of numeric values.
	\param mean (number) The mean of the values.

	\return (number) The standard deviation of the values. Returns 0 if there are
		less than 2 values or if the values table is empty.
--]]

function WFMathUtils.calculateStandardDeviation( values, mean )

	local sumSq = 0
	local count = 0

	 for _, value in pairs( values ) do

		sumSq = sumSq + ( value - mean ) ^ 2
		count = count + 1

	end

	if count > 1 then
		return math.sqrt( sumSq / ( count ) )
	else
		return 0
	end

end

--------------------------------------------------------------------------------

--[[!
	\brief Calculates the median value from a set of values.

	\param values (table) A table of numeric values.

	\return (number) The median value. Returns 0 if the values table is empty.
		For an even number of values, returns the lower of the two middle values.
--]]

function WFMathUtils.calculateMedian( values )

	-- Improve this by avoiding the new table, and just using a custom sort function

	local sortedValues = {}

	for _, value in pairs( values ) do
		sortedValues[ #sortedValues + 1 ] = value
	end

	table.sort( sortedValues )

	local median = sortedValues[ #sortedValues ] or 0
	
	if #sortedValues > 1 then
		median = sortedValues[ math.floor( #sortedValues / 2 ) ]
	end

	return median

end

--------------------------------------------------------------------------------

--[[!
	\brief Standardizes a set of values by converting them to z-scores.

	\param values (table) A table of numeric values.
	\param mean (number) The mean of the values.
	\param stdDev (number) The standard deviation of the values.

	\return (table) A new table containing the standardized values. Each value is
		calculated as (value - mean) / stdDev. If stdDev is 0, the values are
		simply centered around the mean (value - mean).
--]]

function WFMathUtils.standardizeValues( values, mean, stdDev )

	local standardizedValues = {}

	for key, value in pairs( values ) do

		local result = value - mean

		if stdDev ~= 0 then
			standardizedValues[ key ] = result / stdDev
		else
			standardizedValues[ key ] = result
		end

	end

	return standardizedValues

end

--------------------------------------------------------------------------------

--[[!
	\brief Calculates the first and third quartiles of a set of values.

	\param values (table) A table of numeric values.

	\return (number, number) The first and third quartiles of the values.
		If there are less than 2 values, both quartiles will be 0.
		For an even number of values, the quartiles are interpolated between
		the two nearest values.
--]]

function WFMathUtils.calculateQuartiles( values )

	local sortedValues = {}

	for _, value in pairs( values ) do
		sortedValues[ #sortedValues + 1 ] = value
	end

	table.sort( sortedValues )
	local n = #sortedValues

	local function getQuartilePosition( q )
		return ( n + 1 ) * q
	end

	local function interpolateQuartile( pos )

		local lower = math.floor( pos )
		local upper = math.ceil( pos )

		if lower == upper then
			return sortedValues[ lower ]
		else
			return ( sortedValues[ lower ] + sortedValues[ upper ] ) / 2
		end

	end

	local q1Pos = getQuartilePosition( 0.25 )
	local q3Pos = getQuartilePosition( 0.75 )

	local q1 = interpolateQuartile( q1Pos )
	local q3 = interpolateQuartile( q3Pos )

	return q1, q3

end

---------------------------------------------------------------------------------

--[[!
	\brief Calculates the great-circle distance between two points on Earth using the Haversine formula.

	\param lat1 (number) Latitude of the first point in degrees.
	\param lon1 (number) Longitude of the first point in degrees.
	\param lat2 (number) Latitude of the second point in degrees.
	\param lon2 (number) Longitude of the second point in degrees.

	\return (number) The distance between the two points in kilometers.
--]]

function WFMathUtils.haversineDistance( lat1, lon1, lat2, lon2 )

	local R = 6378 -- Radius of the Earth in kilometers

	local dLat = math.rad( lat2 - lat1 )
	local dLon = math.rad( lon2 - lon1 )

	local a =
			math.sin( dLat / 2 ) * math.sin( dLat / 2 ) +
			math.cos( math.rad( lat1 ) ) * math.cos( math.rad( lat2 ) ) *
			math.sin( dLon / 2 ) * math.sin( dLon / 2 )

	local c = 2 * math.atan2( math.sqrt( a ), math.sqrt( 1 - a ) )

	return R * c -- Distance in kilometers

end

--------------------------------------------------------------------------------

--[[!
	\brief Calculates the Haversine distances from a set of points to their mean location.

	\param latitudes (table) A table mapping document IDs to latitude values.
	\param latitudesSum (number) The sum of all latitude values.
	\param longitudes (table) A table mapping document IDs to longitude values.
	\param longitudesSum (number) The sum of all longitude values.
	\param count (number) The total number of points.

	\return (table, number) A tuple containing:
		- A table mapping document IDs to their distance from the mean location
		- The mean distance of all points from the mean location
--]]

function WFMathUtils.getHaversineDistances( latitudes, latitudesSum, longitudes, longitudesSum, count )

	local meanLatitude = latitudesSum / count
	local meanLongitude = longitudesSum / count

	local distances = {}

	local distanceInKmSum = 0

	for docId, _ in pairs( latitudes ) do

		local distanceInKm = WFMathUtils.haversineDistance(
				meanLatitude,
				meanLongitude,
				latitudes[ docId ],
				longitudes[ docId ] )

		distanceInKmSum = distanceInKmSum + distanceInKm

		distances[ docId ] = distanceInKm

	end

	local meanDistance = distanceInKmSum / count

	return distances, meanDistance

end

--------------------------------------------------------------------------------

--[[!
	\brief Rounds a number to one decimal place.

	\param value (number) The number to round.

	\return (number) The number rounded to one decimal place.
--]]

function WFMathUtils.roundToTenths( value )
	return math.floor( value * 10 + 0.5 ) / 10
end

--------------------------------------------------------------------------------

--[[!
	\brief Determines the N-month span for a given date.

	\param date (number) The timestamp to calculate the span for.
	\param spanLengthInMonths (number) The length of each span in months.

	\return (number, number) A tuple containing:
		- The year
		- The span number (1-4 for 3-month spans in a year)
--]]

function WFMathUtils.getDatePeriodIndex( date, spanLengthInMonths )

	-- dates is formatted as: dayOfMonth + month * 100 + year * 10000

	local month = math.floor( date / 100 ) % 100
	local year = math.floor( date / 10000 )
	local span = math.floor( ( month - 1 ) / spanLengthInMonths ) + 1

	return year, span

end

--------------------------------------------------------------------------------

return WFMathUtils
