NUi = {}
NUi.ScaleFactor = 1.0
NUi.Direction = {
	DOWN = 1,
	LEFT = 2,
	RIGHT = 3,
	UP = 4
}

------------------------------------------------------------------------
------------------------ANNOTATION DEFINITIONS--------------------------
------------------------------------------------------------------------

--- @class Coordinates The location of the element on the screen
--- @field x number The x coordinate of the element on the screen
--- @field y number The y coordinate of the element on the screen
---
--- @class Dimensions The dimensions of the element on the screen
--- @field Width number The width of the element on the screen
--- @field Height number The height of the element on the screen
---
--- @class Color The color of the element on the screen. This is primarily used when monochrome is active.
--- @field Red number The amount of red in the color
--- @field Green number The amount of green in the color
--- @field Blue number The amount of blue in the color
--- @field Alpha number The amount of transparency in the color
---
--- @class ImageInformation The color of the element on the screen. This is primarily used when monochrome is active.
--- @field x1 number The x coordinate of the top-left corner of the bar based on direction. 
--- @field x2 number The y coordinate of the top-left corner of the bar based on direction.
--- @field y1 number The x coordinate of the bottom-right corner of the bar based on direction.
--- @field y2 number The y coordinate of the bottom-right corner of the bar based on direction.
--- @field uvx1 number The x coordinate of the top-left corner of the bar image. This will crop the image based on the factor, so for example if factor is 0.5 and direction is 
--- @field uvx2 number The y coordinate of the top-left corner of the bar image. This will typically be 0 unless the bar image is designed to be cropped vertically as well.
--- @field uvy1 number The x coordinate of the bottom-right corner of the bar image. This will crop the image based on the factor, so for example if factor is 0.5 and direction
--- @field uvy2 number The y coordinate of the bottom-right corner of the bar image. This will typically be 1 unless the bar image is designed to be cropped vertically as well.
--- 
--- @class ElementInfo The information required to render the element on the screen
--- @field Position Coordinates The location of the element on the screen
--- @field Offset Coordinates The offset of the element on the screen from the location
--- @field Dimensions Dimensions The dimensions of the element on the screen
--- @field Image string|texture The image|texture that will be rendered on the screen
--- @field Color Color The color of the element on the screen. This is primarily used when monochrome is active
--- @field Percentage number The percentage of the image|texture shown on screen
--- @field Direction string The direction in which the element will be shown on screen. 
--- @field Monochrome? boolean Sets whether or not the image|texture is black and white and will be filled with the given color
--- @field Inverse? boolean Sets whether the percentage needs inversed. Useful for changing elements from getting smaller to elements getting larger (such as bars)

------------------------------------------------------------------------
---------------------------LOCAL FUNCTIONS------------------------------
------------------------------------------------------------------------

--- Gets the positioning information of the bar based on the provided information and parameters. This includes the coordinates for where the bar should be drawn, as well as the UV coordinates for how the bar image should be stretched or cropped. The direction parameter determines which way the bar fills (right, left, up, down), and the offset parameter allows for adjusting the position of the bar relative to its base position.
--- @param elementInfo ElementInfo The information about the element
--- @param fullWidth number The full length of the bar. This is typically the displayLevel multiplied by the full length of the bar (either width or height depending on direction).
--- @return ImageInformation
local function GetBarCoords(elementInfo, fullWidth)
	local x1, y1, x2, y2, uvx1, uvy1, uvx2, uvy2
	if (not elementInfo.Direction) or (elementInfo.Direction == NUi.Direction.RIGHT) then
		x1 = elementInfo.Position.x + elementInfo.Offset.x
		x2 = x1 + (elementInfo.Percentage * fullWidth)
		y1 = elementInfo.Position.y + elementInfo.Offset.y
		y2 = y1 + elementInfo.Dimensions.Height
		uvx1 = 0
		uvy1 = 0
		uvx2 = elementInfo.Percentage
		uvy2 = 1
		
	elseif elementInfo.Direction == NUi.Direction.LEFT then
		x2 = elementInfo.Position.x + elementInfo.Offset.x + fullWidth
		x1 = x2 - (elementInfo.Percentage * fullWidth)
		y1 = elementInfo.Position.y + elementInfo.Offset.y
		y2 = y1 + elementInfo.Dimensions.Height
		uvx1 = 1 - elementInfo.Percentage
		uvy1 = 0
		uvx2 = 1
		uvy2 = 1
	
	elseif elementInfo.Direction == NUi.Direction.UP then
		x1 = elementInfo.Position.x + elementInfo.Offset.x
		x2 = x1 + elementInfo.Dimensions.Width
		y1 = elementInfo.Position.y + elementInfo.Offset.y
		y2 = y1 + (elementInfo.Percentage * fullWidth)
		uvx1 = 0
		uvy1 = 0
		uvx2 = 1
		uvy2 = elementInfo.Percentage
	
	elseif elementInfo.Direction == NUi.Direction.DOWN then
		x1 = elementInfo.Position.x + elementInfo.Offset.x
		x2 = x1 + elementInfo.Dimensions.Width
		y2 = elementInfo.Position.y + elementInfo.Offset.y + fullWidth
		y1 = y2 - (elementInfo.Percentage * fullWidth)
		uvx1 = 0
		uvy1 = 1 - elementInfo.Percentage
		uvx2 = 1
		uvy2 = 1
	end

	return { x1=math.floor(x1), y1=math.floor(y1), x2=math.floor(x2), y2=math.floor(y2), uvx1=uvx1, uvy1=uvy1, uvx2=uvx2, uvy2=uvy2 }
end

--- Linear interpolation between 2 numbers based on a percentage
--- @param numberA number The starting number
--- @param numberB number The ending number
--- @param percentage number The percentage used to determine the returned number
--- @return number
local function Lerp(numberA, numberB, percentage)
    return numberA + (numberB - numberA) * percentage
end

--- Lerps between 2 colors
--- @param colorA Color The start color
--- @param colorB Color the end color
--- @param percentage number The percentage between the 2 colors, used to determine the lerped color
--- @return Color
local function LerpColor(colorA, colorB, percentage)
    return {
       	math.floor(Lerp(colorA[1], colorB[1], percentage)),
        math.floor(Lerp(colorA[2], colorB[2], percentage)),
        math.floor(Lerp(colorA[3], colorB[3], percentage)),
        math.floor(Lerp(colorA[4], colorB[4], percentage))
    }
end

------------------------------------------------------------------------
--------------------------PUBLIC FUNCTIONS------------------------------
------------------------------------------------------------------------

--- Draws a HUD bar (such as a health bar) based on the provided information and parameters. The bar will be drawn with the specified image and color, and will be filled according to the display level and full length. The direction parameter determines which way the bar fills (right, left, up, down), and an optional position override can be used to specify exact coordinates for the bar instead of using the calculated ones
--- @param elementInfo ElementInfo
function NUi:DrawHudElement(elementInfo)
	if elementInfo.Monochrome == nil then elementInfo.Monochrome = true end
	if elementInfo.Inverse == nil then elementInfo.Inverse = false end
	if elementInfo.Inverse then
		elementInfo.Percentage = 1 - elementInfo.Percentage
	end

	if elementInfo.Percentage >= 0.01 and elementInfo.Image ~= nil then
		local orientationBarLength
		if (elementInfo.Direction == NUi.Direction.RIGHT) or (elementInfo.Direction == NUi.Direction.LEFT) then
			orientationBarLength = elementInfo.Dimensions.Width
		else
			orientationBarLength = elementInfo.Dimensions.Height
		end
		local drawingDimensions = GetBarCoords(elementInfo, orientationBarLength)
		
		gr.setColor(elementInfo.Color[1], elementInfo.Color[2], elementInfo.Color[3], elementInfo.Color[4])
		gr.drawImage(elementInfo.Image, drawingDimensions.x1, drawingDimensions.y1, drawingDimensions.x2, drawingDimensions.y2,
		drawingDimensions.uvx1, drawingDimensions.uvy1, drawingDimensions.uvx2, drawingDimensions.uvy2, 1, elementInfo.Monochrome)
	end
end

--- Gets the scaling needed to scale the HUD elements to. The default is 1920x1080, but it will scale down if the screen resolution is smaller than that, and will maintain aspect ratio by using the smaller of the width and height scaling factors.
--- @param baseWidth number The base width to scale from (typically the width of the resolution the HUD was designed for, such as 1920).
--- @param baseHeight number The base height to scale from (typically the height of the resolution the HUD was designed for, such as 1080).
--- @return number scalingFactor The factor by which to scale the HUD elements. This is typically used to multiply the coordinates and sizes of the HUD elements to scale them appropriately for the screen resolution.
function NUi:CalculateScaleFactor(baseWidth, baseHeight)
	local scalingFactorWidth
	local scalingFactorHeight
	local scalingFactor

	scalingFactorWidth = gr.getScreenWidth() / baseWidth
	scalingFactorHeight = gr.getScreenHeight() / baseHeight
	scalingFactor = math.min(scalingFactorWidth, scalingFactorHeight)

	return scalingFactor
end

--- Returns the global scale factor computed from the base resolution in nui-hud.tbl.
--- @return number scaleFactor
function NUi:GetScaleFactor()
	return self.ScaleFactor
end

--- Calculates a screen position from a fractional origin and a pixel offset.
--- @param origin number[] Fractional screen coordinates {x, y} (0.0–1.0)
--- @param offset number[] Pixel offset {x, y} applied after the origin
--- @return Coordinates
function NUi:CalculatePosition(origin, offset)
	return {
		x = (gr.getScreenWidth() * origin[1]) + offset[1],
		y = (gr.getScreenHeight() * origin[2]) + offset[2]
	}
end

--- Parses a graphic element block from the current parse position. Reads filename, dimensions, optional origin, optional offset, and calculates screen position. Call this immediately after consuming the element's $Key: tag.
--- @param scaleFactor number Scale factor applied to dimensions and offsets
--- @return table entry with File, Width, Height, Origin, Offset, Position
function NUi:ParseGraphicBlock(scaleFactor)
	local entry = {}
	entry.File = parse.getString()
	entry.Width = gr.getImageWidth(entry.File) * scaleFactor
	entry.Height = gr.getImageHeight(entry.File) * scaleFactor

	if parse.optionalString("+Origin:") then
		entry.Origin = NUi:GetCSV(parse.getString())
	else
		entry.Origin = {0, 0}
	end

	if parse.optionalString("+Offset:") then
		entry.Offset = NUi:GetCSV(parse.getString())
		entry.Offset[1] = entry.Offset[1] * scaleFactor
		entry.Offset[2] = entry.Offset[2] * scaleFactor
	else
		entry.Offset = {0, 0}
	end

	if parse.optionalString("+Scale X:") then
		entry.Width = entry.Width * parse.getFloat()
	end

	if parse.optionalString("+Scale Y:") then
		entry.Height = entry.Height * parse.getFloat()
	end

	if parse.optionalString("+Anchor:") then
		entry.Anchor = (parse.getString():match("^%s*(.-)%s*$") or ""):lower()
	end

	if parse.optionalString("+Inverse:") then
		entry.Inverse = parse.getBoolean()
	else
		entry.Inverse = false
	end

	if parse.optionalString("+Anim Time:") then
		entry.AnimTime = parse.getFloat()
	else
		entry.AnimTime = 0
	end

	entry.Position = NUi:CalculatePosition(entry.Origin, entry.Offset)

	if entry.Anchor == "right" or entry.Anchor == "bottomright" then
		entry.Position.x = entry.Position.x - entry.Width
	end
	if entry.Anchor == "bottom" or entry.Anchor == "bottomright" then
		entry.Position.y = entry.Position.y - entry.Height
	end

	return entry
end

--- Parses a color block from the current parse position. Reads either threshold colors (+Good, +Warning, +Bad) or a repeating list of lerp colors (+Color), plus an optional label color (+Label). Call this immediately after consuming the block's $Key: tag.
--- @return table block with Mode ("threshold" or "lerp"), color fields, and Label
function NUi:ParseColorBlock()
	local block = {}

	if parse.optionalString("+Good:") then
		block.Mode = "threshold"
		local rgba = NUi:GetCSV(parse.getString())
		block.Good = {rgba[1], rgba[2], rgba[3], rgba[4] or 255}

		if parse.optionalString("+Warning:") then
			rgba = NUi:GetCSV(parse.getString())
			block.Warning = {rgba[1], rgba[2], rgba[3], rgba[4] or 255}
		else
			block.Warning = block.Good
		end

		if parse.optionalString("+Bad:") then
			rgba = NUi:GetCSV(parse.getString())
			block.Bad = {rgba[1], rgba[2], rgba[3], rgba[4] or 255}
		else
			block.Bad = block.Good
		end
	else
		block.Mode = "lerp"
		block.Colors = {}
		while parse.optionalString("+Color:") do
			local rgba = NUi:GetCSV(parse.getString())
			table.insert(block.Colors, {rgba[1], rgba[2], rgba[3], rgba[4] or 255})
		end
		if #block.Colors == 0 then
			block.Colors = {{127, 127, 127, 255}}
		end
	end

	if parse.optionalString("+Background:") then
		local rgba = NUi:GetCSV(parse.getString())
		block.Background = {rgba[1], rgba[2], rgba[3], rgba[4] or 255}
	else
		block.Background = {0, 0, 0, 127}
	end

	if parse.optionalString("+Label:") then
		local rgba = NUi:GetCSV(parse.getString())
		block.Label = {rgba[1], rgba[2], rgba[3], rgba[4] or 255}
	else
		block.Label = {0, 0, 0, 50}
	end

	return block
end

--- Resolves a color block to a single color based on percentage. Handles both threshold (Good/Warning/Bad) and lerp (+Color) modes.
--- @param colorBlock table A color block as returned by ParseColorBlock
--- @param percentage number 0.0–1.0
--- @return Color
function NUi:ResolveColor(colorBlock, percentage)
	if not colorBlock then
		return {127, 127, 127, 255}
	end
	if colorBlock.Mode == "threshold" then
		if percentage <= 0.15 then return colorBlock.Bad
		elseif percentage <= 0.33 then return colorBlock.Warning
		else return colorBlock.Good end
	end
	if not colorBlock.Colors then
		return {127, 127, 127, 255}
	end
	if #colorBlock.Colors == 1 then
		return colorBlock.Colors[1]
	end
	return NUi:LerpColors(colorBlock.Colors, percentage)
end

--- Gets the direction based on a passed in string. If none of the exact matches are found, it will default to "right". This is used to convert the direction specified in the configuration (which is a string) into a format that can be used for calculations when drawing the bar.
--- @param direction string The direction as a string, expected values are "right", "left", "up", "down". This is typically passed in from the configuration for the bar.
--- @return NUi.Direction
function NUi:GetDirection(direction)
	direction = (direction:match("^%s*(.-)%s*$") or direction):lower()
	if direction == "left" then
		return NUi.Direction.LEFT
	elseif direction == "up" then
		return NUi.Direction.UP
	elseif direction == "down" then
		return NUi.Direction.DOWN
	else
		return NUi.Direction.RIGHT
	end
end

--- Lerps between the colors given in the table
--- @param colors Color[] List of colors to lerp through
--- @param percentage number The percentage used to determine which 2 colors to lerp.
--- @return Color
function NUi:LerpColors(colors, percentage)
    percentage = math.max(0, math.min(1, percentage)) 
    local numberOfColors = #colors
    if numberOfColors == 1 then return colors[1] end
    
    local segmentRaw = percentage * (numberOfColors - 1)
    local index = math.floor(segmentRaw) + 1
    local localPercentage = segmentRaw - math.floor(segmentRaw)
    
    if index >= numberOfColors then return colors[numberOfColors] end
    return LerpColor(colors[index], colors[index + 1], localPercentage)
end

function NUi:ParseGeneralSettings(file, object)
	NUi:ParseConfigSection(file,"#General Settings", function()
		if parse.optionalString("$Auto Guardian:") then
			object.AutoGuardian = parse.getBoolean()
			
			if parse.optionalString("+Excluded Subsys:") then
				object.ExcludeSubsys = NUi:GetCSV(parse.getString())
			else
				object.ExcludeSubsys = {}
			end
			
		else
			object.AutoGuardian = false
		end
				
		if parse.optionalString("$Show Damage Gauge:") then
			object.ShowSubsystemDamage = parse.getBoolean()
		else
			object.ShowSubsystemDamage = true
		end

		if parse.optionalString("$Low Health Alerts:") then
			object.LowHealthAlerts = parse.getBoolean()
		else
			object.LowHealthAlerts = false
		end

		if parse.optionalString("$Scaling:") then
			object.Scaling = parse.getBoolean()
			
			if parse.optionalString("+Base Resolution:") then
				object.ScalingBase = NUi:GetCSV(parse.getString())
			else
				object.ScalingBase = {1024,768}
			end

			if object.Scaling then
				object.ScaleFactor = NUi:CalculateScaleFactor(object.ScalingBase[1], object.ScalingBase[2])
			else
				object.ScaleFactor = 1
			end
			NUi.ScaleFactor = object.ScaleFactor
			
		else
			object.Scaling = nil
			object.ScaleFactor = 1
		end

		parse.requiredString("#End")
	end)
end

--- Reads a .tbl file, skips to the specified section, and executes a callback function to parse that section. The callback function should contain the logic to read the specific values from that section and apply them as needed.
--- @param tableFileName string The name of the .tbl file to read (without the path, as it will look in "data/tables" by default).
--- @param sectionTitle string The title of the section to skip to (including the # symbol, e.g. "#General Settings").
--- @param callback function The function to execute once the specified section is reached. This function should contain the logic to read the values from that section and apply them as needed.
function NUi:ParseConfigSection(tableFileName, sectionTitle, callback)
	if not parse.readFileText(tableFileName, "data/tables") then
		return
	end
	
	if parse.skipToString(sectionTitle) then
		callback()
	end

	parse.stop()
end

--http://lua-users.org/wiki/LuaCsv
function NUi:GetCSV(line, sep)
	local res = {}
	local pos = 1
	sep = sep or ','
	while true do 
		local c = string.sub(line,pos,pos)
		if (c == "") then break end
		if (c == '"') then
			-- quoted value (ignore separator within)
			local txt = ""
			repeat
				local startp,endp = string.find(line,'^%b""',pos)
				txt = txt..string.sub(line,startp+1,endp-1)
				pos = endp + 1
				c = string.sub(line,pos,pos) 
				if (c == '"') then txt = txt..'"' end 
				-- check first char AFTER quoted string, if it is another
				-- quoted string without separator, then append it
				-- this is the way to "escape" the quote char in a quote. example:
				--   value1,"blub""blip""boing",value3  will result in blub"blip"boing  for the middle
			until (c ~= '"')
			table.insert(res,txt)
			assert(c == sep or c == "")
			pos = pos + 1
		else	
			-- no quotes used, just look for the first separator
			local startp,endp = string.find(line,sep,pos)
			if (startp) then 
				table.insert(res,string.sub(line,pos,startp-1))
				pos = endp + 1
			else
				-- no separator found -> use rest of string and terminate
				table.insert(res,string.sub(line,pos))
				break
			end 
		end
	end
	
	return res
end

--- Resolves a thermal color (grey idle → lerp colors as heat rises). Used by boost and weapon overheat bars.
--- @param colorBlock table A lerp-mode color block as returned by ParseColorBlock
--- @param heatLevel number 0.0–1.0 where 0 = cool/idle, 1 = fully heated
--- @return Color
function NUi:ResolveThermalColor(colorBlock, heatLevel)
	local idleColor = {192, 192, 192, 255}
	local fadeThreshold = 0.02
	if heatLevel < fadeThreshold then
		return NUi:LerpColors({idleColor, colorBlock.Colors[1]}, heatLevel / fadeThreshold)
	else
		return NUi:ResolveColor(colorBlock, heatLevel)
	end
end

function NUi:Ease(easeTime, beginningValue, change, duration)
	return change * easeTime / duration + beginningValue
end

--- Quadratic ease-out: fast start, decelerates to stop. t is 0–1, returns 0–1.
--- @param t number
--- @return number
function NUi:EaseOut(t)
	t = math.max(0, math.min(1, t))
	return 1 - (1 - t) * (1 - t)
end

return NUi