--- Display Specifications - The Lua interface for the C++ DisplaySpecs
-- @module 03_DisplaySpecs

-- A shim to make an honest-to-goodness lua table (array) out of the
-- userdata DisplaySpecs we get from c++
local cachedSpecs = nil

function GetDisplaySpecs()
	if cachedSpecs == nil then
		local specs = DISPLAY:GetDisplaySpecs()
		t = {
			ById= function(self, id)
				for i, d in ipairs(self) do
					if d:GetId() == id or
					((id == '' or id == nil) and d:IsVirtual()) then
						return self[i]
					end
				end
				-- default to returning first DisplaySpec
				return self[1]
			end,
			-- hold on to a reference to the userdata, so it isn't GC'd prematurely
			c_specs= specs
		}
		for i = 1, #specs do
			t[i] = specs[i]
		end

		cachedSpecs = t
	end

	return cachedSpecs
end

-- Ease lookup of OptionRow effects masks
local OE = OptEffect:Reverse()

function ConfDisplayMode()
	local effectsMask = 2^OE["OptEffect_SavePreferences"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyGraphics"]

	local specs = GetDisplaySpecs()
	local choices = {}
	for i, d in ipairs(specs) do
		choices[i] = d:GetName()
	end
	local origDisplay = PREFSMAN:GetPreference("DisplayId")
	local origWindowed = PREFSMAN:GetPreference("Windowed")
	choices[#choices + 1] = "Windowed"

	local t = {
		Name= "Windowed",
		GoToFirstOnStart= false,
		OneChoiceForAllPlayers= true,
		ExportOnChange = false,
		ExportOnCancel = false,
		LayoutType= "ShowAllInRow",
		SelectType= "SelectOne",

		Choices= choices,
		LoadSelections= function(self, list, pn)
			local windowed = PREFSMAN:GetPreference("Windowed")
			local displayIdx = FindValue(specs, specs:ById(PREFSMAN:GetPreference("DisplayId")))
			local i = windowed and #self.Choices or displayIdx
			list[i] = true
		end,
		SaveSelections= function(self, list, pn)
			local choice = FindSelection(list)
			local selWindowed = choice == #self.Choices
			local selDisplay = selWindowed and origDisplay or specs[choice]:GetId()

			-- If we've changed from the established preferences in place at our
			-- instantiation, then we'll return our effects
			if selWindowed ~= origWindowed
			or selDisplay ~= origDisplay then
				return effectsMask
			else
				return 0
			end
		end,
		NotifyOfSelection= function(self, pn, choice)
			local curDisplay = PREFSMAN:GetPreference("DisplayId")
			local curWindowed = PREFSMAN:GetPreference("Windowed")
			local selWindowed = choice == #self.Choices
			local selDisplay = selWindowed and curDisplay or specs[choice]:GetId()
			-- If we've changed from what's currently saved in PREFSMAN,
			-- then we'll broadcast that state
			if selWindowed ~= curWindowed
			or selDisplay ~= curDisplay then
				PREFSMAN:SetPreference("Windowed", selWindowed)
				PREFSMAN:SetPreference("DisplayId", selDisplay)
				MESSAGEMAN:Broadcast("DisplayChoiceChanged")
			end
		end
	}
	return t
end

local winFracs = {
	{n=3, d=4},
	{n=1, d=1},
	{n=5, d=4},
	{n=4, d=3},
	{n=16, d=10},
	{n=16, d=9},
	{n=8, d=3},
	{n=21, d=9}
}
-- Maximum distance between aspect ratios to be considered equivalent
-- (approximately half the distance between 16:9 and 16:10)
local RATIO_EPSILON = .044


function GetWindowAspectRatios()
	local ratios = {}
	for i, f in ipairs(winFracs) do
		ratios[math.round(f.n/f.d, 3)] = f
	end

	return ratios
end

-- build a map from DisplayId to supported aspect ratios
function GetDisplayAspectRatios(specs)
	local ratios = {}
	local recognized = GetWindowAspectRatios()
	for _, d in ipairs(specs) do
		local dRatios = {}
		for j, mode in ipairs(d:GetSupportedModes()) do
			local trueRatio = mode:GetWidth()/mode:GetHeight()
			local truncRatio = math.round(trueRatio, 3)
			if not dRatios[truncRatio] then
				local f = {}
				-- Check if this is a known aspect ratio, or practically the
				-- same (e.g. 1366x768 is typically considered 16:9)
				for knownRatio, fraction in pairs(recognized) do
					if math.abs(trueRatio - (fraction.n/fraction.d)) < RATIO_EPSILON then
						truncRatio = knownRatio
						f = fraction
						break
					end
				end

				-- If we didn't find a recognized ratio, then register a new ratio
				-- as the reduced fraction of width / height
				if not f.n then
					local w = mode:GetWidth()
					local h = mode:GetHeight()
					local gcd = math.gcd(w, h)
					dRatios[trueRatio] = {n=w/gcd, d=h/gcd}
				else
					dRatios[truncRatio] = f
				end
			end
		end
		ratios[d:GetId()] = dRatios
		-- Allow looking up the virtual display's aspect ratio by empty string
		if d:IsVirtual() then
			ratios[""] = dRatios
		end
	end

	return ratios
end

-- hack, use as a space to share value between ConfAspectRatio() and
-- ConfDisplayResoltuion(). Can't use PREFSMAN:SetPreference("DisplayAspectRatio", x)
-- because that change take effect *immediately* (it is read in the rendering loop)
local CurAspectRatio = 0

function ConfAspectRatio()
	local effectsMask = 2^OE["OptEffect_SavePreferences"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyGraphics"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyAspectRatio"]
	local specs = GetDisplaySpecs()
	local origAspectRatio = PREFSMAN:GetPreference("DisplayAspectRatio")
	CurAspectRatio = origAspectRatio

	local dRatios = GetDisplayAspectRatios(specs)
	local wRatios = GetWindowAspectRatios()
	local t = {
		Name= "DisplayAspectRatio",
		GoToFirstOnStart= false,
		OneChoiceForAllPlayers= true,
		ExportOnChange = false,
		ExportOnCancel = false,
		LayoutType= "ShowAllInRow",
		SelectType= "SelectOne",
		ReloadRowMessages= {"DisplayChoiceChanged"},
		LoadSelections= function(self, list, pn)
			local r = CurAspectRatio
			for i, v in ipairs(self.ChoiceVals) do
				if math.abs(CurAspectRatio - v) < RATIO_EPSILON then
					list[i] = true
					break
				end
			end
		end,

		SaveSelections= function(self, list, pn)
			local i = FindSelection(list)
			PREFSMAN:SetPreference("DisplayAspectRatio", self.ChoiceVals[i])
			if math.abs(self.ChoiceVals[i] - origAspectRatio) < RATIO_EPSILON then
				return 0
			else
				return effectsMask
			end
		end,

		NotifyOfSelection= function(self, pn, choice)
			local selRatio = self.ChoiceVals[choice]
			if math.abs(selRatio-CurAspectRatio) >= RATIO_EPSILON then
				CurAspectRatio = selRatio
				MESSAGEMAN:Broadcast("AspectRatioChoiceChanged")
			end
		end,

		Reload= function(self)
			local origVals = self.ChoiceVals
			self:GenChoices()

			-- Pass along the message to ConfDisplayResolution()
			-- regardless, but important we do this *after* self:GenChoices()
			-- to ensure we've updated the CurAspectRatio in accordance
			-- with what this display supports
			MESSAGEMAN:Broadcast("AspectRatioChoiceChanged")

			if #origVals ~= #self.ChoiceVals then
				return "ReloadChanged_All"
			end

			for i, v in ipairs(origVals) do
				if origVals[i] ~= self.ChoiceVals[i] then
					return "ReloadChanged_All"
				end
			end

			return "ReloadChanged_None"
		end,

		GenChoices= function(self)
			-- Determine the available aspect ratios for the current display mode
			local isWindowed = PREFSMAN:GetPreference("Windowed")
			local curDisplayId = PREFSMAN:GetPreference("DisplayId")
			local ratios = isWindowed and wRatios or (dRatios[curDisplayId] or dRatios[specs[1]:GetId()])
			local choices = {}
			local vals = {}
			foreach_ordered(ratios, function(v, f)
						vals[#vals + 1] = f.n/f.d
						choices[#choices + 1] = tostring(f.n) .. ':' .. tostring(f.d)
			end)
			self.Choices = choices
			self.ChoiceVals = vals

			-- Get the closest aspect ratio to the currently-configured one
			-- that this display mode supports
			local closestRatio = vals[1]
			local closestDist = math.abs(vals[1] - CurAspectRatio)
			for _, v in ipairs(vals) do
				local dist = math.abs(v - CurAspectRatio)
				if dist < closestDist then
					closestRatio = v
					closestDist = dist
				end
			end
			CurAspectRatio = closestRatio
		end
	}
	t:GenChoices()
	return t
end

local function GenerateFeasibleWindowSizesForRatio(specs, r)
	-- Get the largest feasible window size *for a single monitor*,
	-- then generate a sequence of smaller window sizes, until
	-- one of height/width < 300 (arbitrary)
	-- TODO: Get fancy and consider larger rectangles that span
	-- multiple monitors
	local maxWinWidth = 0
	for _, d in ipairs(specs) do
		local modes = d:GetSupportedModes()
		local dLargestMode = modes[#modes]
		local w = dLargestMode:GetWidth()
		local h = dLargestMode:GetHeight()
		local dMaxWinWidth = w/r <= h and w or r*h
		maxWinWidth = dMaxWinWidth > maxWinWidth and dMaxWinWidth or maxWinWidth
	end

	-- Generate successively smaller rectangles
	local sizes = {}

	local w = maxWinWidth
	local h = maxWinWidth/r
	while w > 300 and h > 300 do
		sizes[#sizes + 1] = {w=math.round(w), h=math.round(h)}
		w = 0.75*w
		h = w/r
	end

	-- Reverse the list (sort in increasing size)
	local rsizes = {}
	for i = #sizes, 1, -1 do
		rsizes[i] = sizes[#sizes - i + 1]
	end

	return rsizes
end

function GetFeasibleWindowSizesForRatio(specs, r)
	-- First, try to see if we have a sufficiently large set of
	-- resolutions to pick from in the list(s) of supported modes for the display(s)
	-- (since those resolutions tend to be "natural" looking), otherwise just generate
	-- some window sizes (which might look weird).
	local widths = {}
	for _, d in ipairs(specs) do
		for _, m in ipairs(d:GetSupportedModes()) do
			local w = m:GetWidth()
			local h = m:GetHeight()
			if math.abs((w/h) - r) < RATIO_EPSILON then
				widths[w] = h
			end
		end
	end

	if table.itemcount(widths) < 4 then
		return GenerateFeasibleWindowSizesForRatio(specs, r)
	else
		local resolutions = {}
		for w, h in pairs(widths) do
			resolutions[#resolutions + 1] = {w=w, h=h}
		end
		table.sort(resolutions, function (a, b) return a.w < b.w end)
		return resolutions
	end

end


function GetDisplayResolutionsForRatio(d, r)
	local sizes = {}
	for _, m in ipairs(d:GetSupportedModes()) do
		local w = m:GetWidth()
		local h = m:GetHeight()
		local trueRatio = w/h
		if math.abs(trueRatio - r) < RATIO_EPSILON then
			-- This depends on the fact that GetSupportedModes returns
			-- a *sorted* list
			if #sizes == 0 or sizes[#sizes].w ~= w or sizes[#sizes].h ~= h then
				sizes[#sizes + 1] = {w=m:GetWidth(), h=m:GetHeight()}
			end
		end
	end
	return sizes
end

function ConfDisplayResolution()
	local effectsMask = 2^OE["OptEffect_SavePreferences"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyGraphics"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyAspectRatio"]
	local specs = GetDisplaySpecs()
	local origWidth = PREFSMAN:GetPreference("DisplayWidth")
	local origHeight = PREFSMAN:GetPreference("DisplayHeight")

	local t = {
		Name= "DisplayResolution",
		GoToFirstOnStart= false,
		OneChoiceForAllPlayers= true,
		ExportOnChange = false,
		ExportOnCancel = false,
		LayoutType= "ShowAllInRow",
		SelectType= "SelectOne",
		ReloadRowMessages= {"AspectRatioChoiceChanged"},
		LoadSelections= function(self, list, pn)
			local w = PREFSMAN:GetPreference("DisplayWidth")
			local h = PREFSMAN:GetPreference("DisplayHeight")
			for i, v in ipairs(self.ChoiceVals) do
				if v.w == w and v.h == h then
					list[i] = true
					break
				end
			end
		end,

		SaveSelections= function(self, list, pn)
			local i = FindSelection(list)
			local w = self.ChoiceVals[i].w
			local h = self.ChoiceVals[i].h
			PREFSMAN:SetPreference("DisplayWidth", w)
			PREFSMAN:SetPreference("DisplayHeight", h)
			if w == origWidth and h == origHeight then
				return 0
			else
				return effectsMask
			end
		end,

		NotifyOfSelection= function(self, pn, choice)
			local selRes = self.ChoiceVals[choice]
			local curWidth = PREFSMAN:GetPreference("DisplayWidth")
			local curHeight = PREFSMAN:GetPreference("DisplayHeight")
			if selRes.w ~= curWidth or selRes.h ~= curHeight then
				PREFSMAN:SetPreference("DisplayWidth", selRes.w)
				PREFSMAN:SetPreference("DisplayHeight", selRes.h)
				MESSAGEMAN:Broadcast("DisplayResolutionChoiceChanged")
			end
		end,

		Reload= function(self)
			local origVals = self.ChoiceVals
			self:GenChoices()

			-- Pass along the message to ConfRefreshRate() regardless of
			-- what happens here. Do this *after* GenChoices() so that
			-- *if* currently configured resolution needed to change in response
			-- to change in configured AspectRatio, ConfRefreshRate() can change
			-- accordingly
			MESSAGEMAN:Broadcast("DisplayResolutionChoiceChanged")

			if #origVals ~= #self.ChoiceVals then
				return "ReloadChanged_All"
			end

			for i, v in ipairs(origVals) do
				if origVals[i] ~= self.ChoiceVals[i] then
					return "ReloadChanged_All"
				end
			end

			return "ReloadChanged_None"
		end,

		GenChoices= function(self)
			local isWindowed = PREFSMAN:GetPreference("Windowed")
			local curDisplay = specs:ById(PREFSMAN:GetPreference("DisplayId"))
			local curRatio = CurAspectRatio ~= 0 and CurAspectRatio or PREFSMAN:GetPreference("DisplayAspectRatio")
			local resolutions = isWindowed and GetFeasibleWindowSizesForRatio(specs, curRatio)
				or GetDisplayResolutionsForRatio(curDisplay, curRatio)

			local choices = {}
			local vals = {}
			for i, r in ipairs(resolutions) do
				vals[#vals + 1] = r
				choices[#choices + 1] = tostring(r.w) .. 'x' .. tostring(r.h)
			end
			self.Choices = choices
			self.ChoiceVals = vals

			-- If the currently configured resolution isn't available at this
			-- aspect ratio, then pick the closest resolution
			local w = PREFSMAN:GetPreference("DisplayWidth")
			local h = PREFSMAN:GetPreference("DisplayHeight")
			local closest = nil
			local mindist = -1
			for i, v in ipairs(vals) do
				local dist = math.sqrt((v.w - w)^2 + (v.h - h)^2)
				if mindist == -1 or dist < mindist then
					mindist = dist
					closest = v
				end
			end
			PREFSMAN:SetPreference("DisplayWidth", closest.w)
			PREFSMAN:SetPreference("DisplayHeight", closest.h)
		end
	}
	t:GenChoices()
	return t
end

function GetDisplayRatesForResolution(d, w, h)
	local rates = {}
	for _, m in ipairs(d:GetSupportedModes()) do
		if m:GetWidth() == w and m:GetHeight() == h then
			rates[#rates + 1] = math.round(m:GetRefreshRate())
		end
	end
	return rates
end

function ConfRefreshRate()
	local effectsMask = 2^OE["OptEffect_SavePreferences"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyGraphics"]
	local specs = GetDisplaySpecs()
	local origRate = PREFSMAN:GetPreference("RefreshRate")

	local t = {
		Name= "RefreshRate",
		GoToFirstOnStart= false,
		OneChoiceForAllPlayers= true,
		ExportOnChange = false,
		ExportOnCancel = false,
		LayoutType= "ShowAllInRow",
		SelectType= "SelectOne",
		ReloadRowMessages= {"DisplayResolutionChoiceChanged"},
		LoadSelections= function(self, list, pn)
			local r = PREFSMAN:GetPreference("RefreshRate")
			local i = FindValue(self.ChoiceVals, r)
			list[i] = true
		end,

		SaveSelections= function(self, list, pn)
			local i = FindSelection(list)
			local r = self.ChoiceVals[i]
			PREFSMAN:SetPreference("RefreshRate", r)
			if r ~= origRate then
				return effectsMask
			else
				return 0
			end
		end,

		NotifyOfSelection= function(self, pn, choice)
			local selRate = self.ChoiceVals[choice]
			local curRate = PREFSMAN:GetPreference("RefreshRate")
			if selRate ~= curRate then
				PREFSMAN:SetPreference("RefreshRate", selRate)
			end
		end,

		Reload= function(self)
			local origVals = self.ChoiceVals
			self:GenChoices()
			if #origVals ~= #self.ChoiceVals then
				return "ReloadChanged_All"
			end

			for i, v in ipairs(origVals) do
				if origVals[i] ~= self.ChoiceVals[i] then
					return "ReloadChanged_All"
				end
			end

			return "ReloadChanged_None"
		end,

		GenChoices= function(self)
			local isWindowed = PREFSMAN:GetPreference("Windowed")
			local d = specs:ById(PREFSMAN:GetPreference("DisplayId"))
			local w = PREFSMAN:GetPreference("DisplayWidth")
			local h = PREFSMAN:GetPreference("DisplayHeight")
			local rates = isWindowed and {}
				or GetDisplayRatesForResolution(d, w, h)

			local choices = {"Default"}
			local choiceVals = {REFRESH_DEFAULT}
			for i, r in ipairs(rates) do
				choiceVals[#choiceVals + 1] = math.round(r)
				choices[#choices + 1] = tostring(math.round(r))
			end

			self.Choices = choices
			self.ChoiceVals = choiceVals

			-- If the currently configured refresh rate isn't available at this
			-- resolution, then pick the closest one (so long as it's within
			-- a fixed threshold)
			local curRate = PREFSMAN:GetPreference("RefreshRate")
			local threshold = 10
			local closestIdx = nil
			local mindist = -1
			for i, r in ipairs(choiceVals) do
				local dist = math.abs(r - curRate)
				if mindist == -1 or dist < mindist then
					mindist = dist
					closestIdx = i
				end
			end
			local curRateIdx = mindist < threshold and closestIdx or 1
			PREFSMAN:SetPreference("RefreshRate", choiceVals[curRateIdx])
		end
	}
	t:GenChoices()
	return t
end

function ConfFullscreenType()
	local effectsMask = 2^OE["OptEffect_SavePreferences"]
	effectsMask = effectsMask + 2^OE["OptEffect_ApplyGraphics"]

	local FULLSCREEN_EXCLUSIVE = 0
	local FULLSCREEN_BORDERLESS_WIN = 1

	local fsbwSupported = DISPLAY:SupportsFullscreenBorderlessWindow()

	local choiceVals = {FULLSCREEN_EXCLUSIVE}
	local choices = {"Default"}
	if fsbwSupported then
		choices = {THEME:GetString("OptionNames", "ExclusiveFullscreen"), THEME:GetString("OptionNames", "BorderlessWindow")}
		choiceVals = {FULLSCREEN_EXCLUSIVE, FULLSCREEN_BORDERLESS_WIN}
	end
	local origFSType = PREFSMAN:GetPreference("FullscreenIsBorderlessWindow") and FULLSCREEN_BORDERLESS_WIN or FULLSCREEN_EXCLUSIVE

	local t = {
		Name= "FullscreenType",
		GoToFirstOnStart= false,
		OneChoiceForAllPlayers= true,
		ExportOnChange = false,
		ExportOnCancel = false,
		LayoutType= "ShowAllInRow",
		SelectType= "SelectOne",
		Choices= choices,
		ChoiceVals= choiceVals,

		LoadSelections= function(self, list, pn)
			local fsType = PREFSMAN:GetPreference("FullscreenIsBorderlessWindow") and FULLSCREEN_BORDERLESS_WIN
				or FULLSCREEN_EXCLUSIVE
			local i = FindValue(self.ChoiceVals, fsType)
			list[i] = true
		end,
		SaveSelections= function(self, list, pn)
			local selFSType = self.ChoiceVals[FindSelection(list)]
			PREFSMAN:SetPreference("FullscreenIsBorderlessWindow", selFSType == FULLSCREEN_BORDERLESS_WIN)
			if selFSType ~= origFSType then
				return effectsMask
			else
				return 0
			end
		end
	}

	return t
end