local Libra = LibStub("Libra")
local Type, Version = "OptionsFrame", 4
if Libra:GetModuleVersion(Type) >= Version then return end

Libra.modules[Type] = Libra.modules[Type] or {}

local Options = Libra.modules[Type]

Options.Prototype = Options.Prototype or CreateFrame("Frame")
Options.ParentPrototype = Options.ParentPrototype or {}
Options.controls = Options.controls or {}
Options.controlData = Options.controlData or {}

local mt = {__index = Options.Prototype}
local parentMT = {__index = setmetatable(Options.ParentPrototype, {__index = Options.Prototype})}

local Prototype = Options.Prototype
local ParentPrototype = Options.ParentPrototype

local function createFrame(name, parent)
	local frame = CreateFrame("Frame")
	frame.name = name
	frame.parent = parent
	InterfaceOptions_AddCategory(frame)
	
	local title = frame:CreateFontString(nil, nil, "GameFontNormalLarge")
	title:SetPoint("TOPLEFT", 16, -16)
	title:SetPoint("RIGHT", -16, 0)
	title:SetJustifyH("LEFT")
	title:SetJustifyV("TOP")
	title:SetText(name)
	frame.title = title
	
	local desc = frame:CreateFontString(nil, nil, "GameFontHighlightSmall")
	desc:SetHeight(32)
	desc:SetPoint("TOPLEFT", frame.title, "BOTTOMLEFT", 0, -8)
	desc:SetPoint("RIGHT", -31, 0)
	desc:SetJustifyH("LEFT")
	desc:SetJustifyV("TOP")
	desc:SetNonSpaceWrap(true)
	frame.desc = desc
	
	return frame
end

local function constructor(self, name)
	local frame = setmetatable(createFrame(name), parentMT)
	frame.controls = {}
	frame.allcontrols = {}
	return frame
end


function ParentPrototype:AddSubCategory(name, inherit)
	local frame = setmetatable(createFrame(name, self.name), mt)
	if inherit then
		frame.db = self.db
		frame.useProfile = self.useProfile
		frame.handler = self.handler
		frame.allcontrols = self.allcontrols
	else
		frame.allcontrols = {}
	end
	frame.inherit = inherit
	frame.controls = {}
	self.subCategories = self.subCategories or {}
	tinsert(self.subCategories, frame)
	return frame
end

function Prototype:SetDescription(text)
	self.desc:SetText(text)
end

function Prototype:SetDatabase(database, useProfile)
	self.db = database
	self.useProfile = useProfile
	if self.subCategories then
		for i, v in ipairs(self.subCategories) do
			if v.inherit then
				v.db = database
			end
		end
	end
end

function Prototype:SetHandler(tbl)
	self.handler = tbl
	if self.subCategories then
		for i, v in ipairs(self.subCategories) do
			if v.inherit then
				v.handler = tbl
			end
		end
	end
end


local function getTable(control)
	local tbl = control.parent.db
	if control.parent.useProfile then
		tbl = tbl.profile
	end
	if control.keyTable then
		tbl = tbl[control.keyTable]
	end
	return tbl
end

local function getFunc(control, method, key, value)
	local func = control[method]
	if func then
		local object = control
		if type(func) == "string" then
			object = control.parent.handler
			func = object[func]
		end
		if key then
			return true, func(object, key, value)
		else
			return true, func(object, value)
		end
		return true
	end
end

local function set(self, value, key)
	if not getFunc(self, "set", key, value) then
		local tbl = getTable(self)
		if tbl then
			tbl[key or self.key] = value
		end
	end
	getFunc(self, "func", key, value)
	for key, control in pairs(self.parent.allcontrols) do
		if control.disabled then
			control:SetEnabled(not control.disabled())
		end
	end
end

local function get(self, key)
	local hasFunc, value = getFunc(self, "get", key)
	if hasFunc then
		return value
	else
		local tbl = getTable(self)
		if tbl then
			return tbl[key or self.key]
		end
	end
end

local controls = Options.controls
local controlData = Options.controlData

do	-- CheckButton
	controlData.CheckButton = {
		x = -2,
		y = -16,
		bottomOffset = 8,
	}
	
	local function onClick(self)
		local checked = self:GetChecked()
		PlaySound(checked and "igMainMenuOptionCheckBoxOn" or "igMainMenuOptionCheckBoxOff")
		set(self, checked)
	end
	
	controls.CheckButton = function(parent)
		local checkButton = CreateFrame("CheckButton", nil, parent, "OptionsBaseCheckButtonTemplate")
		checkButton:SetNormalFontObject("GameFontHighlight")
		checkButton:SetDisabledFontObject("GameFontDisable")
		checkButton:SetPushedTextOffset(0, 0)
		checkButton:SetScript("OnClick", onClick)
		checkButton.SetValue = checkButton.SetChecked
		
		checkButton.label = checkButton:CreateFontString()
		checkButton.label:SetPoint("LEFT", checkButton, "RIGHT", 0, 1)
		checkButton:SetFontString(checkButton.label)
		
		return checkButton
	end
end

do	-- ColorButton
	controlData.ColorButton = {
		x = 3,
		y = -21,
		bottomOffset = 3,
	}
	
	local ColorPickerFrame = ColorPickerFrame
	
	local function setColor(self, color)
		self.swatch:SetVertexColor(color.r, color.g, color.b)
	end
	
	local function saveColor(self, r, g, b)
		self.swatch:SetVertexColor(r, g, b)
		local color = get(self)
		color.r = r
		color.g = g
		color.b = b
		set(self, color)
	end
	
	local function swatchFunc()
		saveColor(ColorPickerFrame.extraInfo, ColorPickerFrame:GetColorRGB())
	end
	
	local function cancelFunc(prev)
		saveColor(ColorPickerFrame.extraInfo, ColorPicker_GetPreviousValues())
	end
	
	local scripts = {
		OnClick = function(self)
			local info = UIDropDownMenu_CreateInfo()
			local color = get(self)
			info.r, info.g, info.b = color.r, color.g, color.b
			info.swatchFunc = swatchFunc
			info.cancelFunc = cancelFunc
			info.extraInfo = self
			OpenColorPicker(info)
		end,
		
		OnEnter = function(self)
			self.bg:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
			if self.tooltipText then
				GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
				GameTooltip:SetText(self.tooltipText, nil, nil, nil, nil, true)
			end
		end,
		
		OnLeave = function(self)
			self.bg:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
			GameTooltip:Hide()
		end,
		
		OnEnable = function(self)
			if self:IsMouseOver() then
				self:OnEnter()
			else
				self.bg:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
			end
		end,
		
		OnDisable = function(self)
			self.bg:SetVertexColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
		end,
	}
	
	controls.ColorButton = function(parent, data)
		local colorButton = CreateFrame("Button", nil, parent)
		colorButton:SetSize(16, 16)
		colorButton:SetNormalFontObject("GameFontHighlight")
		colorButton:SetDisabledFontObject("GameFontDisable")
		colorButton:SetPushedTextOffset(0, 0)
		for script, handler in pairs(scripts) do
			colorButton:SetScript(script, handler)
			colorButton[script] = handler
		end
		colorButton.SetValue = setColor
		
		colorButton:SetNormalTexture([[Interface\ChatFrame\ChatFrameColorSwatch]])
		colorButton.swatch = colorButton:GetNormalTexture()
		
		colorButton.bg = colorButton:CreateTexture(nil, "BACKGROUND")
		colorButton.bg:SetSize(14, 14)
		colorButton.bg:SetPoint("CENTER")
		colorButton.bg:SetTexture(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b)
		
		colorButton.label = colorButton:CreateFontString()
		colorButton.label:SetPoint("LEFT", colorButton, "RIGHT", 5, 1)
		colorButton:SetFontString(colorButton.label)
		
		return colorButton
	end
end

do	-- Slider
	controlData.Slider = {
		x = 7,
		y = -27,
		bottomOffset = -5,
	}
	
	local function onValueChanged(self, value, isUserInput)
		if isUserInput then
			set(self, value)
		end
		if self.isPercent then
			self.currentValue:SetFormattedText("%.0f%%", value * 100)
		else
			self.currentValue:SetText(value)
		end
	end
	
	local function onMinMaxChanged(self, min, max)
		if self.minText or not self.isPercent then
			self.min:SetText(self.minText or min)
		else
			self.min:SetFormattedText("%.0f%%", min * 100)
		end
		if self.maxText or not self.isPercent then
			self.max:SetText(self.maxText or max)
		else
			self.max:SetFormattedText("%.0f%%", max * 100)
		end
	end
	
	controls.Slider = function(parent, data)
		local slider = Libra:CreateSlider(parent)
		slider:SetScript("OnValueChanged", onValueChanged)
		slider:SetScript("OnMinMaxChanged", onMinMaxChanged)
		slider.isPercent = data.isPercent
		slider.minText = data.minText
		slider.maxText = data.maxText
		slider:SetMinMaxValues(data.min, data.max)
		slider:SetValueStep(data.step)
		return slider
	end
end

do	-- Dropdown
	controlData.Dropdown = {
		x = -15,
		y = -32,
		bottomOffset = 8,
	}
	
	local function getValue(dropdown, property, value)
		local properties = dropdown.properties
		local property = properties and properties[property]
		if not properties or not property then
			return value
		else
			if type(property) == "function" then
				return property(value)
			elseif type(property) == "table" then
				return property[value]
			else
				return property
			end
		end
	end
	
	local function setText(self, value)
		self:SetText(getValue(self, "text", value))
	end
	
	local defaultProperties = {
		"text",
		"value",
		"arg1",
	}
	
	local function onClick(self, arg1, arg2, checked)
		if self.owner.multiSelect then
			set(self.owner, checked, arg1)
		else
			self.owner:SetText(self:GetText())
			set(self.owner, arg1)
		end
	end
	
	local function checked(self)
		if self.owner.multiSelect then
			return get(self.owner, self.arg1)
		else
			return self.arg1 == get(self.owner)
		end
	end
	
	local function initialize(self, level, menuList)
		menuList = menuList or self.menulist
		if type(menuList) == "function" then
			menuList = menuList()
		end
		for i, v in ipairs(menuList) do
			local info = UIDropDownMenu_CreateInfo()
			info.func = onClick
			info.checked = checked
			info.isNotRadio = self.multiSelect
			for i, propertyName in ipairs(defaultProperties) do
				info[propertyName] = getValue(self, propertyName, v)
			end
			if self.properties then
				for propertyName in pairs(self.properties) do
					info[propertyName] = getValue(self, propertyName, v)
				end
			end
			self:AddButton(info)
		end
	end
	
	controls.Dropdown = function(parent, data)
		local dropdown = Libra:CreateDropdown("Frame", parent)
		dropdown:JustifyText("LEFT")
		dropdown.SetValue = setText
		dropdown.initialize = data.initialize or initialize
		dropdown.menulist = data.menuList
		dropdown.multiSelect = data.multiSelect
		if data.properties then
			dropdown.properties = {}
			for k, v in pairs(data.properties) do
				dropdown.properties[k] = v
			end
		end
		return dropdown
	end
end

function Libra:AddControlType(type, constructor, controlData)
	if Options.controls[type] then
		error(format("Control type '%s' already exists.", type), 2)
	end
	Options.controls[type] = constructor
	controlData = controlData or {}
	controlData.x = controlData.x or 0
	controlData.y = controlData.y or 0
	controlData.bottomOffset = controlData.bottomOffset or 0
	Options.controlData[type] = controlData
end

function Prototype:CreateOptions(options)
	for i, option in ipairs(options) do
		local control = controls[option.type](self, option)
		local data = controlData[option.type]
		if i == 1 then
			control:SetPoint("TOPLEFT", self.desc, "BOTTOMLEFT", data.x, data.y + 8)
		elseif option.newColumn then
			control:SetPoint("TOPLEFT", self.desc, "BOTTOM", data.x - 2, data.y + 8)
		else
			local previousOption = options[i - 1]
			local previousData = controlData[previousOption.type]
			control:SetPoint("TOPLEFT", self.controls[i - 1], "BOTTOMLEFT", data.x - previousData.x, data.y + previousData.bottomOffset - (option.padding or 0))
		end
		if option.width then
			control:SetWidth(option.width)
		end
		control.parent = self
		control.label:SetText(option.text)
		control.tooltipText = option.tooltip
		control.key = option.key
		control.keyTable = option.keyTable
		control.set = option.set
		control.get = option.get
		control.func = option.func
		control.disabled = option.disabled
		tinsert(self.controls, control)
		tinsert(self.allcontrols, control)
	end
end

function Prototype:SetupControls()
	for i, control in ipairs(self.allcontrols) do
		local value = get(control)
		control:SetValue(value)
		getFunc(control, "func", key, value)
		if control.disabled then
			control:SetEnabled(not control.disabled())
		end
	end
end

function Prototype:GetControlByKey(key, keyTable)
	for i, control in ipairs(self.allcontrols) do
		if control.key == key and control.keyTable == keyTable then
			return control
		end
	end
end

Libra:RegisterModule(Type, Version, constructor)