--- @type 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);