---@class StdUi
local StdUi = LibStub and LibStub('StdUi', true)
if not StdUi then
	return
end

local module, version = 'Builder', 6
if not StdUi:UpgradeNeeded(module, version) then
	return
end

local util = StdUi.Util

local function setDatabaseValue(db, key, value)
	if key:find('.') then
		local accessor = StdUi.Util.stringSplit('.', key)
		local startPos = db

		for i, subKey in pairs(accessor) do
			if i == #accessor then
				startPos[subKey] = value
				return
			end

			startPos = startPos[subKey]
		end
	else
		db[key] = value
	end
end

local function getDatabaseValue(db, key)
	if key:find('.') then
		local accessor = StdUi.Util.stringSplit('.', key)
		local startPos = db

		for i, subKey in pairs(accessor) do
			if i == #accessor then
				return startPos[subKey]
			end

			startPos = startPos[subKey]
		end
	else
		return db[key]
	end
end

---BuildElement
---@param frame Frame
---@param row EasyLayoutRow
---@param info table
---@param dataKey string
---@param db table
function StdUi:BuildElement(frame, row, info, dataKey, db)
	local element

	local genericChangeEvent = function(el, value)
		setDatabaseValue(el.dbReference, el.dataKey, value)
		if el.onChange then
			el:onChange(value)
		end
	end

	local hasLabel = false
	if info.type == 'checkbox' then
		element = self:Checkbox(frame, info.label)
	elseif info.type == 'editBox' then
		element = self:EditBox(frame, nil, 20)
	elseif info.type == 'multiLineBox' then
		element = self:MultiLineBox(frame, 300, 20)
	elseif info.type == 'dropdown' then
		element = self:Dropdown(frame, 300, 20, info.options or {}, nil, info.multi or nil, info.assoc or false)
	elseif info.type == 'autocomplete' then
		element = self:Autocomplete(frame, 300, 20, '')

		if info.validator then
			element.validator = info.validator
		end
		if info.transformer then
			element.transformer = info.transformer
		end
		if info.buttonCreate then
			element.buttonCreate = info.buttonCreate
		end
		if info.buttonUpdate then
			element.buttonUpdate = info.buttonUpdate
		end
		if info.items then
			element:SetItems(info.items)
		end
	elseif info.type == 'slider' or info.type == 'sliderWithBox' then
		element = self:SliderWithBox(frame, nil, 32, 0, info.min or 0, info.max or 2)

		if info.precision then
			element:SetPrecision(info.precision)
		end
	elseif info.type == 'color' then
		element = self:ColorInput(frame, info.label, 100, 20, info.color)
	elseif info.type == 'button' then
		element = self:Button(frame, nil, 20, info.text or '')

		if info.onClick then
			element:SetScript('OnClick', info.onClick)
		end
	elseif info.type == 'header' then
		element = self:Header(frame, info.label)
	elseif info.type == 'label' then
		element = self:Label(frame, info.label)
	elseif info.type == 'texture' then
		element = self:Texture(frame, info.width or 24, info.height or 24, info.texture)
	elseif info.type == 'panel' then -- Containers
		element = self:Panel(frame, 300, 20)
	elseif info.type == 'scroll' then
		element = self:ScrollFrame(frame, 300, 20, type(info.scrollChild) == 'table' and info.scrollChild or nil)
		if type(info.scrollChild) == 'function' then
			info.scrollChild(element)
		end
	elseif info.type == 'fauxScroll' then
		element =
			self:FauxScrollFrame(
			frame,
			300,
			20,
			info.displayCount or 5,
			info.lineHeight or 22,
			type(info.scrollChild) == 'table' and info.scrollChild or nil
		)
		if type(info.scrollChild) == 'function' then
			info.scrollChild(element)
		end
	elseif info.type == 'tab' then
		element = self:TabPanel(frame, 300, 20, info.tabs or {}, info.vertical or false, info.buttonWidth, info.buttonHeight)
	elseif info.type == 'custom' then
		element = info.createFunction(frame, row, info, dataKey, db)
	end

	if not element then
		print('Could not build element with type: ', info.type)
	end

	-- Widgets can have initialization code
	if info.init then
		info.init(element)
	end

	element.dbReference = db
	element.dataKey = dataKey
	if info.onChange then
		element.onChange = info.onChange
	end

	if element.hasLabel then
		hasLabel = true
	end

	local canHaveLabel =
		info.type ~= 'checkbox' and info.type ~= 'header' and info.type ~= 'label' and info.type ~= 'color'

	if info.label and canHaveLabel then
		self:AddLabel(frame, element, info.label)
		hasLabel = true
	end

	if info.initialValue then
		if element.SetChecked then
			element:SetChecked(info.initialValue)
		elseif element.SetColor then
			element:SetColor(info.initialValue)
		elseif element.SetValue then
			element:SetValue(info.initialValue)
		end
	end

	-- Setting onValueChanged disqualifies from any writes to database
	if info.onValueChanged then
		element.OnValueChanged = info.onValueChanged
	elseif db then
		local iVal = getDatabaseValue(db, dataKey)

		if info.type == 'checkbox' then
			element:SetChecked(iVal)
		elseif element.SetColor then
			element:SetColor(iVal)
		elseif element.SetValue then
			element:SetValue(iVal)
		end

		element.OnValueChanged = genericChangeEvent
	end

	-- Technically, every frame can be a container
	if info.children then
		self:BuildWindow(element, info.children)
		self:EasyLayout(element, {padding = {top = 10}})

		element:SetScript(
			'OnShow',
			function(of)
				of:DoLayout()
			end
		)
	end

	row:AddElement(
		element,
		{
			column = info.column or 12,
			fullSize = info.fullSize or false,
			fullHeight = info.fullHeight or false,
			margin = info.layoutMargins or
				{
					top = (hasLabel and 20 or 0)
				}
		}
	)

	return element
end

---BuildRow
---@param frame Frame
---@param info table
---@param db table
function StdUi:BuildRow(frame, info, db)
	local row = frame:AddRow()

	for key, element in util.orderedPairs(info) do
		local dataKey = element.key or key or nil

		local el = self:BuildElement(frame, row, element, dataKey, db)
		if element then
			if not frame.elements then
				frame.elements = {}
			end

			frame.elements[key] = el
		end
	end
end

---BuildWindow
---@param frame Frame
---@param info table
function StdUi:BuildWindow(frame, info)
	local db = info.database or nil

	assert(info.rows, 'Rows are required in order to build table')
	local rows = info.rows

	self:EasyLayout(frame, info.layoutConfig)

	for _, row in util.orderedPairs(rows) do
		self:BuildRow(frame, row, db)
	end

	frame:DoLayout()
end

StdUi:RegisterModule(module, version)
