--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

local L = pace.LanguageString

local languageID = CreateClientConVar("pac_editor_languageid", 1, true, false, "Whether we should show the language indicator inside of editable text entries.")

function pace.ShowSpecial(pnl, parent, size)
	size = size or 150

	pnl:SetPos(pace.Editor:GetWide(), select(2, parent:LocalToScreen()) - size + 25)
	pnl:SetSize(size, size)
	pnl:MakePopup()
end

function pace.FixMenu(menu)
	menu:SetMaxHeight(500)
	menu:InvalidateLayout(true, true)
	menu:SetPos(pace.Editor:GetPos() + pace.Editor:GetWide(), gui.MouseY() - (menu:GetTall() * 0.5))
end

local function DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight)
	local btn = vgui.Create("DButton", self)
	btn:SetSize(16, 16)
	btn:Dock(RIGHT)
	btn:SetText("...")
	btn.DoClick = function() callFuncLeft(self, self.CurrentKey) end

	if callFuncRight then
		btn.DoRightClick = function() callFuncRight(self, self.CurrentKey) end
	else
		btn.DoRightClick = btn.DoClick
	end

	if self.OnMoreOptionsLeftClickButton then
		self:OnMoreOptionsLeftClickButton(btn)
	end

	return btn
end

function pace.CreateSearchList(property, key, name, add_columns, get_list, get_current, add_line, select_value, select_value_search)
	select_value = select_value or function(val, key) return val end
	select_value_search = select_value_search or select_value
	pace.SafeRemoveSpecialPanel()

	local frame = vgui.Create("DFrame")
	frame:SetTitle(L(name))
	frame:SetSize(300, 300)
	frame:Center()
	frame:SetSizable(true)

	local list = vgui.Create("DListView", frame)
	list:Dock(FILL)
	list:SetMultiSelect(false)

	add_columns(list)

	list.OnRowSelected = function(_, id, line)
		local val = select_value(line.list_val, line.list_key)

		if property and property:IsValid() then
			property:SetValue(val)
			property.OnValueChanged(val)
		else
			if pace.current_part:IsValid() and pace.current_part["Set" .. key] then
				pace.Call("VariableChanged", pace.current_part, key, val)
			end
		end
	end

	local first = NULL

	local function build(find)
		list:Clear()

		local cur = get_current()
		local newList = {}

		for k, v in pairs(get_list()) do
			table.insert(newList, {k, v, tostring(k), tostring(v)})
		end

		table.sort(newList, function(a, b) return a[1] < b[1] end)
		if find then find = find:lower() end

		for i, data in ipairs(newList) do
			local key, val, keyFriendly, valFriendly = data[1], data[2], data[3], data[4]
			if (not find or find == "") or tostring(select_value_search(valFriendly, keyFriendly)):lower():find(find, nil, true) then

				local pnl = add_line(list, key, val)
				pnl.list_key = key
				pnl.list_val = val

				if not first:IsValid() then
					first = pnl
				end

				if cur == name then
					list:SelectItem(pnl)
				end
			end
		end
	end

	local search = vgui.Create("DTextEntry", frame)
	search:Dock(BOTTOM)
	search.OnTextChanged = function() build(search:GetValue()) end
	search.OnEnter = function() if first:IsValid() then list:SelectItem(first) end frame:Remove() end
	search:RequestFocus()
	frame:MakePopup()

	build()

	pace.ActiveSpecialPanel = frame

	return frame
end

pace.ActiveSpecialPanel = NULL
pace.extra_populates = {}

function pace.SafeRemoveSpecialPanel()
	if pace.ActiveSpecialPanel:IsValid() then
		pace.ActiveSpecialPanel:Remove()
	end
end

pac.AddHook("GUIMousePressed", "pace_SafeRemoveSpecialPanel", function()
	local pnl = pace.ActiveSpecialPanel
	if pnl:IsValid() then
		local x,y = input.GetCursorPos()
		local _x, _y = pnl:GetPos()
		if x < _x or y < _y or x > _x + pnl:GetWide() or y > _y + pnl:GetTall() then
			pnl:Remove()
		end
	end
end)

do -- container
	local PANEL = {}

	PANEL.ClassName = "properties_container"
	PANEL.Base = "DPanel"

	function PANEL:Paint(w, h)
		--surface.SetDrawColor(255, 255, 255, 255)
		--surface.DrawRect(0,0,w,h)
		--self:GetSkin().tex.CategoryList.Outer(0, 0, w, h)

		--self:GetSkin().tex.MenuBG(0, 0, w + (self.right and -1 or 3), h + 1)

		if not self.right then
			--surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
			--surface.DrawRect(0,0,w+5,h)
		else
			--surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
			--surface.DrawRect(0,0,w,h)
		end

		self.AltLine = self.alt_line
		derma.SkinHook( "Paint", "CategoryButton", self, w, h )
	end

	function PANEL:SetContent(pnl)
		pnl:SetParent(self)
		self.content = pnl
	end

	function PANEL:PerformLayout()
		local pnl = self.content or NULL
		if pnl:IsValid() then
			pnl:SetPos(0, 0)
			pnl:SetSize(self:GetSize())
		end
	end

	pace.RegisterPanel(PANEL)
end

do -- list
	local PANEL = {}

	PANEL.ClassName = "properties"
	PANEL.Base = "Panel"

	AccessorFunc(PANEL, "item_height", "ItemHeight")

	function PANEL:Init()

		local search = vgui.Create("DTextEntry", self)
		search:Dock(TOP)
		search.Kill = function()
			search:SetVisible(false)
			search.searched_something = false
			search:SetText("")
			search:SetEnabled(false)

			for i,v in ipairs(self.List) do
				v.left:SetVisible(true)
				v.right:SetVisible(true)
			end
		end

		search.OnEnter = search.Kill

		search.OnTextChanged = function()
			self.scr:SetScroll(0)

			local pattern = search:GetValue()
			if pattern == "" and search.searched_something then
				search:Kill()
				search:KillFocus()
				pace.Editor:KillFocus()
				pace.Editor:MakePopup()
			else
				search.searched_something = true
				local group

				for i,v in ipairs(self.List) do
					local found = false

					if v.panel then
						if v.panel:GetText():find(pattern) then
							found = true
						end

						if v.left:GetValue():find(pattern) then
							found = true
						end
					elseif v.left and v.left.text then
						group = v.left.text
					end

					if group and group:find(pattern) then
						found = true
					end

					if not found and v.panel then
						v.left:SetVisible(false)
						v.right:SetVisible(false)
					end
				end

				for i,v in ipairs(self.List) do
					if not v.panel then
						local hide_group = true

						for i = i+1, #self.List do
							local val = self.List[i]
							if not val.panel then
								break
							end

							if val.left:IsVisible() then
								hide_group = false
								break
							end
						end

						if hide_group then
							v.left:SetVisible(false)
							v.right:SetVisible(false)
						end
					end
				end
			end
		end
		search:SetVisible(false)
		self.search = search

		self.List = {}

		local divider = vgui.Create("DHorizontalDivider", self)

		local left = vgui.Create("DPanelList", divider)
			divider:SetLeft(left)
		self.left = left

		local right = vgui.Create("DPanelList", divider)
			divider:SetRight(right)
		self.right = right

		divider:SetDividerWidth(3)

		surface.SetFont(pace.CurrentFont)
		local w,h = surface.GetTextSize("W")
		local size = h + 2

		self:SetItemHeight(size)

		self.div = divider

		function divider:PerformLayout()
			DHorizontalDivider.PerformLayout(self)

			if self.m_pLeft then
				self.m_pLeft:SetWide( self.m_iLeftWidth + self.m_iDividerWidth )
			end
		end

		local scroll = vgui.Create("DVScrollBar", self)
		scroll:Dock(RIGHT)
		self.scr = scroll

		left.OnMouseWheeled = function(_, delta) scroll:OnMouseWheeled(delta) end
		--right.OnMouseWheeled = function(_, delta) scroll:OnMouseWheeled(delta) end
	end

	function PANEL:GetHeight(hack)
		return (self.item_height * (#self.List+(hack or 1))) - (self.div:GetDividerWidth() + 1)
	end

	function PANEL:PerformLayout()
		self.scr:SetSize(10, self:GetHeight())
		self.scr:SetUp(self:GetTall(), self:GetHeight() - 10)
		self.search:SetZPos(1)
		self.div:SetPos(0, (self.search:IsVisible() and self.search:GetTall() or 0) + self.scr:GetOffset())
		local w, h = self:GetSize()
		local scroll_width = self.scr.Enabled and self.scr:GetWide() or 0
		self.div:SetLeftWidth((w/2) - scroll_width)
		self.div:SetSize(w - scroll_width, self:GetHeight())
	end

	function PANEL:Paint(w, h)
		self:GetSkin().tex.CategoryList.Outer(0, 0, w, h)
	end

	pace.CollapsedProperties = pace.luadata.ReadFile("pac3_editor/collapsed.txt") or {}

	function PANEL:AddCollapser(name)
		assert(name)
		for i,v in ipairs(self.List) do
			if v.group == name then
				return
			end
		end

		local left = vgui.Create("DButton", self)
		left:SetTall(self:GetItemHeight())
		left:SetText("")
		left.text = name

		self.left:AddItem(left)

		left.DoClick = function()
			pace.CollapsedProperties[name] = not pace.CollapsedProperties[name]
			pace.PopulateProperties(pace.current_part)

			pace.Editor:InvalidateLayout()
			pace.luadata.WriteFile("pac3_editor/collapsed.txt", pace.CollapsedProperties)
		end

		left.GetValue = function() return name end

		local right = vgui.Create("DButton", self)
		right:SetTall(self:GetItemHeight())
		right:SetText("")
		self.right:AddItem(right)

		right.DoClick = left.DoClick

		left.Paint = function(_, w, h)
			--surface.SetDrawColor(left:GetSkin().Colours.Category.Header)
			--surface.DrawRect(0,0,w*2,h)
			left:GetSkin().tex.CategoryList.Header( 0, 0, w*2, h )

			surface.SetFont(pace.CurrentFont)

			local txt = L(name)
			local _, _h = surface.GetTextSize(txt)
			local middle = h/2 - _h/2

			--surface.SetTextPos(11, middle)
			--surface.SetTextColor(derma.Color("text_dark", self, color_black))
			--surface.SetFont(pace.CurrentFont)
			--surface.DrawText(txt)
			draw.TextShadow({text = txt, font = pace.CurrentFont, pos = {11, middle}, color = left:GetSkin().Colours.Category.Header}, 1, 100)

			local txt = (pace.CollapsedProperties[name] and "+" or "-")
			local w = surface.GetTextSize(txt)
			draw.TextShadow({text = txt, font = pace.CurrentFont, pos = {6-w*0.5, middle}, color = left:GetSkin().Colours.Category.Header}, 1, 100)

		end

		right.Paint = function(_,w,h)
			left:GetSkin().tex.CategoryList.Header(-w,0,w*2,h)
		end

		table.insert(self.List, {left = left, right = right, panel = var, key = key, group = name})

		return #self.List
	end

	function PANEL:AddKeyValue(key, var, pos, obj, udata, group)
		local btn = pace.CreatePanel("properties_label")
			btn:SetTall(self:GetItemHeight())

			do
				local key = key
				if key:EndsWith("UID") then
					key = key:sub(1, -4)
				end

				btn:SetValue(L((udata and udata.editor_friendly or key):gsub("%u", " %1"):lower()):Trim())
			end

			if obj then
				btn.key_name = key
				btn.part_namepart_name = obj.ClassName
			end



		local pnl = pace.CreatePanel("properties_container")
		pnl:SetTall(self:GetItemHeight())
		pnl.right = true
		pnl.alt_line = #self.List%2 == 1
		btn.alt_line = pnl.alt_line

		if ispanel(var) then
			pnl:SetContent(var)
		end

		self.left:AddItem(btn)
		self.right:AddItem(pnl)

		local pos

		if group then
			for i, v in ipairs(self.List) do
				if v.group == group then
					for i = i + 1, #self.List do
						local v = self.List[i]
						if v.group or not v then
							pos = i
							break
						end
					end
				end
			end
		end

		if pos then
			table.insert(self.left.Items, pos, table.remove(self.left.Items))
			table.insert(self.right.Items, pos, table.remove(self.right.Items))

			table.insert(self.List, pos, {left = btn, right = pnl, panel = var, key = key})
		else
			table.insert(self.List, {left = btn, right = pnl, panel = var, key = key})
		end
	end

	function PANEL:Clear()
		for key, data in pairs(self.List) do
			data.left:Remove()
			data.right:Remove()
		end

		self.left:Clear()
		self.right:Clear()

		self.List = {}
	end

	local function FlatListToGroups(list)
		local temp = {}

		for _, prop in ipairs(list) do
			if prop.udata.hidden then continue end

			local group = prop.udata.group or "generic"
			temp[group] = temp[group] or {}
			table.insert(temp[group], prop)
		end

		return temp
	end

	local function SortGroups(groups)
		local out = {}

		local temp = {}
		table.Add(temp, pac.GroupOrder[pace.current_part.ClassName] or {})
		table.Add(temp, pac.GroupOrder.none)
		local done = {}
		for i, name in ipairs(temp) do
			for group, props in pairs(groups) do
				if group == name then
					if not done[group] then
						table.insert(out, {group = group, props = props})
						done[group] = true
					end
				end
			end
		end

		for group, props in pairs(groups) do
			if not done[group] then
				table.insert(out, {group = group, props = props})
			end
		end

		return out
	end

	function PANEL:Populate(flat_list)
		self:Clear()

		for _, data in ipairs(SortGroups(FlatListToGroups(flat_list))) do
			self:AddCollapser(data.group or "generic")
			for pos, prop in ipairs(data.props) do

				if prop.udata and prop.udata.hide_in_editor then
					continue
				end

				local val = prop.get()
				local T = type(val):lower()

				if prop.udata and prop.udata.editor_panel then
					T = prop.udata.editor_panel or T
				elseif pace.PanelExists("properties_" .. prop.key:lower()) then
					T = prop.key:lower()
				elseif not pace.PanelExists("properties_" .. T) then
					T = "string"
				end

				if pace.CollapsedProperties[prop.udata.group] ~= nil and pace.CollapsedProperties[prop.udata.group] then goto CONTINUE end

				local pnl = pace.CreatePanel("properties_" .. T)

				if pnl.PostInit then
					pnl:PostInit()
				end

				if prop.udata and prop.udata.description then
					pnl:SetTooltip(L(prop.udata.description))
				end

				local part = pace.current_part
				part.pace_properties = part.pace_properties or {}
				part.pace_properties[prop.key] = pnl
				pnl.part = part
				pnl.udata = prop.udata

				if prop.udata.enums then
					DefineMoreOptionsLeftClick(pnl, function(self)
						pace.CreateSearchList(
							self,
							self.CurrentKey,
							L(prop.key),

							function(list)
								list:AddColumn("enum")
							end,

							function()
								local tbl

								if isfunction(prop.udata.enums) then
									if pace.current_part:IsValid() then
										tbl = prop.udata.enums(pace.current_part)
									end
								else
									tbl = prop.udata.enums
								end

								local enums = {}

								if tbl then
									for k, v in pairs(tbl) do
										if not isstring(v) then
											v = k
										end

										if not isstring(k) then
											k = v
										end

										enums[k] = v
									end
								end

								return enums
							end,

							function()
								return pace.current_part[prop.key]
							end,

							function(list, key, val)
								return list:AddLine(key)
							end,

							function(val, key)
								return val
							end
						)
					end)
				end
				if prop.udata.editor_sensitivity or prop.udata.editor_clamp or prop.udata.editor_round then
					pnl.LimitValue = function(self, num)
						if prop.udata.editor_sensitivity then
							self.sens = prop.udata.editor_sensitivity
						end
						if prop.udata.editor_clamp then
							num = math.Clamp(num, unpack(prop.udata.editor_clamp))
						end
						if prop.udata.editor_round then
							num = math.Round(num)
						end
						return num
					end
				elseif prop.udata.editor_onchange then
					pnl.LimitValue = prop.udata.editor_onchange
				end

				pnl.CurrentKey = prop.key

				if pnl.ExtraPopulate then
					table.insert(pace.extra_populates, {pnl = pnl, func = pnl.ExtraPopulate})
					pnl:Remove()
					goto CONTINUE
				end

				pnl:SetValue(val)

				pnl.OnValueChanged = function(val)
					if T == "number" then
						val = tonumber(val) or 0
					elseif T == "string" then
						val = tostring(val)
					end

					pace.Call("VariableChanged", pace.current_part, prop.key, val)
				end

				self:AddKeyValue(prop.key, pnl, pos, flat_list, prop.udata)

				::CONTINUE::
			end
		end
	end

	pace.RegisterPanel(PANEL)
end

do -- non editable string
	local DTooltip = _G.DTooltip
	if DTooltip and DTooltip.PositionTooltip then
		pace_Old_PositionTooltip = pace_Old_PositionTooltip or DTooltip.PositionTooltip
		function DTooltip.PositionTooltip(self, ...)
			if self.TargetPanel.pac_tooltip_hack then
				local args = {pace_Old_PositionTooltip(self, ...)}

				if (  not IsValid( self.TargetPanel ) ) then
					self:Remove()
					return;
				end

				self:PerformLayout()

				local x, y      = input.GetCursorPos()
				local w, h      = self:GetSize()

				local lx, ly    = self.TargetPanel:LocalToScreen( 0, 0 )

				y = math.min( y, ly - h )

				self:SetPos( x, y )


				return unpack(args)
			end

			return pace_Old_PositionTooltip(self, ...)
		end
	end


	local PANEL = {}

	PANEL.ClassName = "properties_label"
	PANEL.Base = "pace_properties_container"

	function PANEL:SetValue(str)
		local lbl = vgui.Create("DLabel")
			lbl:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
			lbl:SetFont(pace.CurrentFont)
			lbl:SetText(str)
			lbl:SetTextInset(10, 0)
			lbl:SizeToContents()
			lbl.pac_tooltip_hack = true
			self.lbl = lbl
		self:SetContent(lbl)

		if self.part_name and self.key_name then
			lbl.OnCursorEntered = function()

				if lbl.wiki_info then
					lbl:SetTooltip(lbl.wiki_info)
					return
				end

				if not lbl.fetching_wiki then
					lbl:SetCursor("waitarrow")
					pace.GetPropertyDescription(self.part_name, self.key_name, function(str)
						if lbl:IsValid() then
							lbl:SetTooltip(str)
							ChangeTooltip(lbl)
							lbl.wiki_info = str
							lbl:SetCursor("arrow")
						end
					end)
					lbl.fetching_wiki = true
				end
			end
		end
	end

	function PANEL:GetValue()
		return self.lbl:GetValue()
	end

	pace.RegisterPanel(PANEL)
end

do -- base editable
	local PANEL = {}

	PANEL.ClassName = "properties_base_type"
	PANEL.Base = "DLabel"

	PANEL.SingleClick = true

	function PANEL:OnCursorMoved()
		self:SetCursor("hand")
	end

	function PANEL:OnValueChanged()

	end

	function PANEL:Init(...)
		if DLabel and DLabel.Init then
			local status = DLabel.Init(self, ...)
			self:SetText('')
			self:SetMouseInputEnabled(true)
			return status
		end

		return status
	end

	function PANEL:PostInit()
		if self.MoreOptionsLeftClick then
			self:DefineMoreOptionsLeftClick(self.MoreOptionsLeftClick, self.MoreOptionsRightClick)
		end
	end

	function PANEL:DefineMoreOptionsLeftClick(callFuncLeft, callFuncRight)
		return DefineMoreOptionsLeftClick(self, callFuncLeft, callFuncRight)
	end

	function PANEL:SetValue(var, skip_encode)
		if self.editing then return end

		local value = skip_encode and var or self:Encode(var)
		if isnumber(value) then
			-- visually round numbers so 0.6 doesn't show up as 0.600000000001231231 on wear
			value = math.Round(value, 7)
		end
		local str = tostring(value)

		self:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
		self:SetFont(pace.CurrentFont)
		self:SetText("  " .. str) -- ugh
		self:SizeToContents()

		if #str > 10 then
			self:SetTooltip(str)
		else
			self:SetTooltip()
		end

		self.original_str = str
		self.original_var = var

		if self.OnValueSet then
			self:OnValueSet(var)
		end
	end

	-- kind of a hack
	local last_focus = NULL

	function PANEL:OnMousePressed(mcode)
		if last_focus:IsValid() then
			last_focus:Reset()
			last_focus = NULL
		end

		if mcode == MOUSE_LEFT then
			--if input.IsKeyDown(KEY_R) then
			--  self:Restart()
			--else
				self.MousePressing = true
				if self:MousePress(true) == false then return end
				if self.SingleClick or (self.last_press or 0) > RealTime() then
					self:EditText()
					self:DoubleClick()
					self.last_press = 0

					last_focus = self
				else
					self.last_press = RealTime() + 0.2
				end
			--end
		end

		if mcode == MOUSE_RIGHT then
			local menu = DermaMenu()
			menu:SetPos(input.GetCursorPos())
			menu:MakePopup()
			self:PopulateContextMenu(menu)
		end
	end

	function PANEL:PopulateContextMenu(menu)
		menu:AddOption(L"copy", function()
			pace.clipboard = pac.CopyValue(self:GetValue())
		end):SetImage(pace.MiscIcons.copy)
		menu:AddOption(L"paste", function()
			self:SetValue(pac.CopyValue(pace.clipboard))
			self.OnValueChanged(self:GetValue())
		end):SetImage(pace.MiscIcons.paste)

		--left right swap available on strings (and parts)
		if type(self:GetValue()) == 'string' then
			menu:AddSpacer()
			menu:AddOption(L"change sides", function()
				local var
				local part
				if self.udata and self.udata.editor_panel == "part" then
					part = pac.GetPartFromUniqueID(pac.Hash(pac.LocalPlayer), self:GetValue())
					var = part:IsValid() and part:GetName()
				else
					var = self:GetValue()
				end

				local var_flip
				if string.match(var, "left") != nil then
					var_flip = string.gsub(var,"left","right")
				elseif string.match(var, "right") != nil then
					var_flip = string.gsub(var,"right","left")
				end

				if self.udata and self.udata.editor_panel == "part" then
					local target = pac.FindPartByName(pac.Hash(pac.LocalPlayer), var_flip or var, pace.current_part)
					self:SetValue(target or part)
					self.OnValueChanged(target or part)
				else
                self:SetValue(var_flip or var)
                self.OnValueChanged(var_flip or var)
            end
		end):SetImage("icon16/arrow_switch.png")

		--numeric sign flip available on numbers
		elseif type(self:GetValue()) == 'number' then
			menu:AddSpacer()
			menu:AddOption(L"flip sign (+/-)", function()
				local val = self:GetValue()
				self:SetValue(-val)
				self.OnValueChanged(self:GetValue())
			end):SetImage("icon16/arrow_switch.png")
		end

		menu:AddSpacer()
		menu:AddOption(L"reset", function()
			if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then
				local val = pac.CopyValue(pace.current_part.DefaultVars[self.CurrentKey])
				self:SetValue(val)
				self.OnValueChanged(val)
			end
		end):SetImage(pace.MiscIcons.clear)
	end

	function PANEL:OnMouseReleased()
		self:MousePress(false)
		self.MousePressing = false
	end

	function PANEL:IsMouseDown()
		if not input.IsMouseDown(MOUSE_LEFT) then
			self.MousePressing = false
		end
		return self.MousePressing
	end

	function PANEL:DoubleClick()

	end

	function PANEL:MousePress()

	end

	function PANEL:Restart()
		self:SetValue(self:Decode(""))
		self.OnValueChanged(self:Decode(""))
	end

	function PANEL:EncodeEdit(str)
		return str
	end

	function PANEL:DecodeEdit(str)
		return str
	end

	function PANEL:EditText()
		local oldText = self:GetText()
		self:SetText("")

		local pnl = vgui.Create("DTextEntry")
		self.editing = pnl
		pnl:SetFont(pace.CurrentFont)
		pnl:SetDrawBackground(false)
		pnl:SetDrawBorder(false)
		pnl:SetText(self:EncodeEdit(self.original_str or ""))
		pnl:SetKeyboardInputEnabled(true)
		pnl:SetDrawLanguageID(languageID:GetBool())
		pnl:RequestFocus()
		pnl:SelectAllOnFocus(true)

		pnl.OnTextChanged = function() oldText = pnl:GetText() end

		local hookID = tostring({})
		local textEntry = pnl
		local delay = os.clock() + 0.1

		pac.AddHook('Think', hookID, function(code)
			if not IsValid(self) or not IsValid(textEntry) then return pac.RemoveHook('Think', hookID) end
			if textEntry:IsHovered() or self:IsHovered() then return end
			if delay > os.clock() then return end
			if not input.IsMouseDown(MOUSE_LEFT) and not input.IsKeyDown(KEY_ESCAPE) then return end
			pac.RemoveHook('Think', hookID)
			self.editing = false
			pace.BusyWithProperties = NULL
			textEntry:Remove()
			self:SetText(oldText)
			pnl:OnEnter()
		end)

		--local x,y = pnl:GetPos()
		--pnl:SetPos(x+3,y-4)
		--pnl:Dock(FILL)
		local x, y = self:LocalToScreen()
		local inset_x = self:GetTextInset()
		pnl:SetPos(x+5 + inset_x, y)
		pnl:SetSize(self:GetSize())
		pnl:SetWide(ScrW())
		pnl:MakePopup()

		pnl.OnEnter = function()
			pace.BusyWithProperties = NULL
			self.editing = false

			pnl:Remove()

			self:SetText(tostring(self:Encode(self:DecodeEdit(pnl:GetText() or ""))), true)
			self.OnValueChanged(self:Decode(self:GetText()))
		end

		local old = pnl.Paint
		pnl.Paint = function(...)
			if not self:IsValid() then pnl:Remove() return end

			surface.SetFont(pnl:GetFont())
			local w = surface.GetTextSize(pnl:GetText()) + 6

			surface.DrawRect(0, 0, w, pnl:GetTall())
			surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
			surface.DrawOutlinedRect(0, 0, w, pnl:GetTall())

			pnl:SetWide(w)

			old(...)
		end

		pace.BusyWithProperties = pnl
	end

	pace.BusyWithProperties = NULL

	local function click()
		if not input.IsMouseDown(MOUSE_LEFT) then return end
		local pnl = pace.BusyWithProperties
		if pnl and pnl ~= true and pnl:IsValid() then
			local x, y = input.GetCursorPos()
			local _x, _y = pnl:GetParent():LocalToScreen()
			if x < _x or y < _y or x > _x + pnl:GetParent():GetWide() or y > _y + pnl:GetParent():GetTall() then
				pnl:OnEnter()
			end
		end
	end

	pac.AddHook("GUIMousePressed", "pace_property_text_edit", click)
	pac.AddHook("VGUIMousePressed", "pace_property_text_edit", click)

	function PANEL:Reset()
		if IsValid(self.editing) then
			self.editing:OnEnter()
			self.editing = false
		else
			self:SetValue(self.original_var)
			self.OnValueChanged(self.original_var)
		end
	end

	function PANEL:GetValue()
		return self.original_var
	end

	function PANEL:Encode(var)
		return var
	end

	function PANEL:Decode(var)
		return var
	end

	function PANEL:PerformLayout()
		self:SetSize(self:GetParent():GetSize())
	end

	pace.RegisterPanel(PANEL)
end

do -- string
	local PANEL = {}

	PANEL.ClassName = "properties_string"
	PANEL.Base = "pace_properties_base_type"

	PANEL.SingleClick = true

	pace.RegisterPanel(PANEL)
end

do -- vector
	local function VECTOR(ctor, type, arg1, arg2, arg3, encode, special_callback, sens)
		local PANEL = {}

		PANEL.ClassName = "properties_" .. type
		PANEL.Base = "pace_properties_container"

		PANEL.vector_type = type

		function PANEL:Init(...)
			self.vector = ctor(0,0,0)

			local left = pace.CreatePanel("properties_number", self)
			local middle = pace.CreatePanel("properties_number", self)
			local right = pace.CreatePanel("properties_number", self)

			left.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end
			middle.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end
			right.PopulateContextMenu = function(_, menu) self:PopulateContextMenu(menu) end

			if encode then
				left.Encode = encode
				middle.Encode = encode
				right.Encode = encode
			end

			if sens then
				left.sens = sens
				middle.sens = sens
				right.sens = sens
			end

			local function on_change(arg1, arg2, arg3)
				local restart = 0

				return function(num)
					self.vector[arg1] = num

					if input.IsKeyDown(KEY_R) then
						self:Restart()
						restart = os.clock() + 0.1
					elseif input.IsKeyDown(KEY_LSHIFT) then
						middle:SetValue(num)
						self.vector[arg2] = num

						right:SetValue(num)
						self.vector[arg3] = num
					end

					if restart > os.clock() then
						self:Restart()
						return
					end

					self.OnValueChanged(self.vector * 1)
					self:InvalidateLayout()

					if self.OnValueSet then
						self:OnValueSet(self.vector * 1)
					end
				end
			end

			left:SetMouseInputEnabled(true)
			left.OnValueChanged = on_change(arg1, arg2, arg3)

			middle:SetMouseInputEnabled(true)
			middle.OnValueChanged = on_change(arg2, arg1, arg3)

			right:SetMouseInputEnabled(true)
			right.OnValueChanged = on_change(arg3, arg2, arg1)

			self.left = left
			self.middle = middle
			self.right = right

			if self.MoreOptionsLeftClick then
				local btn = vgui.Create("DButton", self)
				btn:SetSize(16, 16)
				btn:Dock(RIGHT)
				btn:SetText("...")
				btn.DoClick = function() self:MoreOptionsLeftClick(self.CurrentKey) end
				btn.DoRightClick = self.MoreOptionsRightClick and function() self:MoreOptionsRightClick(self.CurrentKey) end or btn.DoClick

				if type == "color" or type == "color2" then
					btn:SetText("")
					btn.Paint = function(_,w,h)
						if type == "color2" then
							surface.SetDrawColor(self.vector.x*255, self.vector.y*255, self.vector.z*255, 255)
						else
							surface.SetDrawColor(self.vector.x, self.vector.y, self.vector.z, 255)
						end
						surface.DrawRect(0,0,w,h)
						surface.SetDrawColor(self:GetSkin().Colours.Properties.Border)
						surface.DrawOutlinedRect(0,0,w,h)
					end
				end
			end

			self.Paint = function() end
		end

		PANEL.MoreOptionsLeftClick = special_callback

		function PANEL:Restart()
			if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then
				self.vector = pac.CopyValue(pace.current_part.DefaultVars[self.CurrentKey])
			else
				self.vector = ctor(0,0,0)
			end

			self.left:SetValue(self.vector[arg1])
			self.middle:SetValue(self.vector[arg2])
			self.right:SetValue(self.vector[arg3])

			self.OnValueChanged(self.vector * 1)
		end

		function PANEL:PopulateContextMenu(menu)
			menu:AddOption(L"copy", function()
				pace.clipboard = pac.CopyValue(self.vector)
			end):SetImage(pace.MiscIcons.copy)
			menu:AddOption(L"paste", function()
				local val = pac.CopyValue(pace.clipboard)
				if isnumber(val) then
					val = ctor(val, val, val)
				elseif isvector(val) and type == "angle" then
					val = ctor(val.x, val.y, val.z)
				elseif isangle(val) and type == "vector" then
					val = ctor(val.p, val.y, val.r)
				end

				if _G.type(val):lower() == type or type == "color" then
					self:SetValue(val)

					self.OnValueChanged(self.vector * 1)
				end
			end):SetImage(pace.MiscIcons.paste)
			menu:AddSpacer()
			menu:AddOption(L"reset", function()
				if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then
					local val = pac.CopyValue(pace.current_part.DefaultVars[self.CurrentKey])
					self:SetValue(val)
					self.OnValueChanged(val)
				end
			end):SetImage(pace.MiscIcons.clear)
		end

		function PANEL:SetValue(vec)
			self.vector = vec * 1

			self.left:SetValue(math.Round(vec[arg1], 4))
			self.middle:SetValue(math.Round(vec[arg2], 4))
			self.right:SetValue(math.Round(vec[arg3], 4))
		end

		function PANEL:PerformLayout()
			self.left:SizeToContents()
			self.left:SetWide(math.max(self.left:GetWide(), 22))

			self.middle:SizeToContents()
			self.middle:SetWide(math.max(self.middle:GetWide(), 22))

			self.right:SizeToContents()
			self.right:SetWide(math.max(self.right:GetWide(), 22))

			self.middle:MoveRightOf(self.left, 10)
			self.right:MoveRightOf(self.middle, 10)
		end

		function PANEL:OnValueChanged(vec)
		end

		pace.RegisterPanel(PANEL)
	end

	VECTOR(Vector, "vector", "x", "y", "z")
	VECTOR(Angle, "angle", "p", "y", "r")

	local function tohex(vec, color2)
		return color2 and ("#%.2X%.2X%.2X"):format(vec.x * 255, vec.y * 255, vec.z * 255) or ("#%.2X%.2X%.2X"):format(vec.x, vec.y, vec.z)
	end

	local function fromhex(str)
		local r, g, b

		if #str <= 4 then -- Supports "#xxx" and "xxx"
			r, g, b = str:match("#?(.)(.)(.)")

			if r and g and b then
				r, g, b = r .. r, g .. g, b .. b
			end
		elseif #str <= 7 then -- Supports "#xxxxxx" and "xxxxxx"
			r, g, b = str:match("#?(..)(..)(..)")
		end

		if r and g and b then
			return Color(tonumber(r, 16) or 255, tonumber(g, 16) or 255, tonumber(b, 16) or 255)
		end
	end

	local function fromColorStr(str)
		local r1, g1, b1 = str:match("([0-9]+), *([0-9]+), *([0-9]+)")
		local r2, g2, b2 = str:match("([0-9]+) +([0-9]+) +([0-9]+)")

		if r1 and g1 and b1 then
			return Color(tonumber(r1) or 255, tonumber(g1) or 255, tonumber(b1) or 255)
		elseif r2 and g2 and b2 then
			return Color(tonumber(r2) or 255, tonumber(g2) or 255, tonumber(b2) or 255)
		end
	end

	local function uncodeValue(valIn)
		local fromHex = fromhex(valIn)
		local fromShareXColorStr = fromColorStr(valIn)

		return fromHex or fromShareXColorStr
	end

	VECTOR(Vector, "color", "x", "y", "z",
		function(self, num) -- this function needs second argument
			local pnum = tonumber(num)

			if not pnum then
				local uncode = uncodeValue(num)

				if uncode then
					timer.Simple(0, function()
						local parent = self:GetParent()
						parent.left:SetValue(uncode.r, true)
						parent.middle:SetValue(uncode.g, true)
						parent.right:SetValue(uncode.b, true)

						parent.left.OnValueChanged(uncode.r)
						parent.middle.OnValueChanged(uncode.g)
						parent.right.OnValueChanged(uncode.b)
					end)

					return '0'
				end

				return '0'
			end

			return tostring(math.Clamp(math.Round(pnum or 0), 0, 255))
		end,

		function(self)
			pace.SafeRemoveSpecialPanel()

			local dlibbased = vgui.GetControlTable("DLibColorMixer")

			local frm = vgui.Create("DFrame")
			frm:SetTitle("Color")

			pace.ShowSpecial(frm, self, 300)

			if dlibbased then
				frm:SetWide(500)
			end

			local clr = vgui.Create(dlibbased and "DLibColorMixer" or "DColorMixer", frm)
			clr:Dock(FILL)
			clr:SetAlphaBar(false) -- Alpha isn't needed
			clr:SetColor(Color(self.vector.x, self.vector.y, self.vector.z))

			local html_color

			if not dlibbased then
				html_color = vgui.Create("DTextEntry", frm)
				html_color:Dock(BOTTOM)
				html_color:SetText(tohex(self.vector))

				html_color.OnEnter = function()
					local valGet = uncodeValue(html_color:GetValue())

					if valGet then
						clr:SetColor(valGet)
					end
				end
			end

			function clr.ValueChanged(_, newColor) -- Only update values when the Color mixer value changes
				local vec = Vector(newColor.r, newColor.g, newColor.b)
				self.OnValueChanged(vec)
				self:SetValue(vec)

				if not dlibbased then
					html_color:SetText(tohex(vec))
				end
			end

			pace.ActiveSpecialPanel = frm
		end,
		10
	)

	VECTOR(Vector, "color2", "x", "y", "z",
		function(_, num)
			num = tonumber(num) or 0

			if input.IsKeyDown(KEY_LCONTROL) then
				num = math.Round(num)
			end

			return tostring(num)
		end,

		function(self)
			pace.SafeRemoveSpecialPanel()

			local dlibbased = vgui.GetControlTable("DLibColorMixer")

			local frm = vgui.Create("DFrame")
			frm:SetTitle("color")

			pace.ShowSpecial(frm, self, 300)

			if dlibbased then
				frm:SetWide(500)
			end

			local clr = vgui.Create(dlibbased and "DLibColorMixer" or "DColorMixer", frm)
			clr:Dock(FILL)
			clr:SetAlphaBar(false)
			clr:SetColor(Color(self.vector.x * 255, self.vector.y * 255, self.vector.z * 255))

			local html_color

			if not dlibbased then
				html_color = vgui.Create("DTextEntry", frm)
				html_color:Dock(BOTTOM)
				html_color:SetText(tohex(self.vector, true))
				html_color.OnEnter = function()
					local col = uncodeValue(html_color:GetValue())
					if col then
						local vec = col:ToVector()
						clr:SetColor(col)
						self.OnValueChanged(vec)
						self:SetValue(vec)
					end
				end
			end

			function clr.ValueChanged(_, newcolor)
				local vec = Vector(newcolor.r / 255, newcolor.g / 255, newcolor.b / 255)
				self.OnValueChanged(vec)
				self:SetValue(vec)

				if not dlibbased then
					html_color:SetText(tohex(vec, true))
				end
			end

			pace.ActiveSpecialPanel = frm
		end,
		0.25
	)
end

do -- number
	local PANEL = {}

	PANEL.ClassName = "properties_number"
	PANEL.Base = "pace_properties_base_type"

	PANEL.sens = 1

	PANEL.SingleClick = false

	function PANEL:MousePress(bool)
		if bool then
			self.mousey = gui.MouseY()
			self.mousex = gui.MouseX()
			self.oldval = tonumber(self:GetValue()) or 0
		else
			self.mousey = nil
		end
	end

	function PANEL:OnCursorMoved()
		self:SetCursor("sizens")
	end

	function PANEL:SetNumberValue(val)
		if self.LimitValue then
			val = self:LimitValue(val) or val
		end

		val = self:Encode(val)
		self:SetValue(val)
		self.OnValueChanged(tonumber(val))
	end

	function PANEL:OnMouseWheeled(delta)
		if not input.IsKeyDown(KEY_LCONTROL) then delta = delta / 10 end
		if input.IsKeyDown(KEY_LALT) then delta = delta / 10 end
		local val = self:GetValue() + (delta * self.sens)

		self:SetNumberValue(val)
	end

	function PANEL:Think()
		if self:IsMouseDown() then
			local sens = self.sens

			if input.IsKeyDown(KEY_LALT) then
				sens = sens / 10
			end

			local delta = (self.mousey - gui.MouseY()) / 10
			local val = (self.oldval or 0) + (delta * sens)

			if input.IsKeyDown(KEY_R) then
				if pace.current_part and pace.current_part.DefaultVars[self.CurrentKey] then
					val = pace.current_part.DefaultVars[self.CurrentKey]
				end
			end

			self:SetNumberValue(val)

			if gui.MouseY()+1 >= ScrH() then
				self.mousey = 0
				self.oldval = val
				input.SetCursorPos(gui.MouseX(), 0)
			elseif gui.MouseY() <= 0 then
				self.mousey = ScrH()
				self.oldval = val
				input.SetCursorPos(gui.MouseX(), ScrH())
			end
		end
	end

	function PANEL:Encode(num)
		if not tonumber(num) then
			local ok, res = pac.CompileExpression(num)
			if ok then
				num = res() or 0
			end
		end

		num = tonumber(num) or 0

		if self:IsMouseDown() then
			if input.IsKeyDown(KEY_LCONTROL) then
				num = math.Round(num)
			elseif input.IsKeyDown(KEY_PAD_MINUS) or input.IsKeyDown(KEY_MINUS) then
				num = -num
			end


			if input.IsKeyDown(KEY_LALT) then
				num = math.Round(num, 5)
			else
				num = math.Round(num, 3)
			end
		end

		return num
	end

	function PANEL:Decode(str)
		return tonumber(str) or 0
	end

	pace.RegisterPanel(PANEL)
end

do -- boolean
	local PANEL = {}

	PANEL.ClassName = "properties_boolean"
	PANEL.Base = "pace_properties_container"

	function PANEL:Init()
		local chck = vgui.Create("DCheckBox", self)
		chck.OnChange = function()
			if self.during_change then return end
			local b = chck:GetChecked()
			self.OnValueChanged(b)
			self.lbl:SetText(L(tostring(b)))
		end
		self.chck = chck

		local lbl = vgui.Create("DLabel", self)
		lbl:SetFont(pace.CurrentFont)
		lbl:SetTextColor(self.alt_line and self:GetSkin().Colours.Category.AltLine.Text or self:GetSkin().Colours.Category.Line.Text)
		self.lbl = lbl
	end

	function PANEL:Paint() end

	function PANEL:SetValue(b)
		self.during_change = true
		self.chck:SetChecked(b)
		self.chck:Toggle()
		self.chck:Toggle()
		self.lbl:SetText(L(tostring(b)))
		self.during_change = false
	end

	function PANEL:OnValueChanged()

	end

	function PANEL:PerformLayout()
		self.BaseClass.PerformLayout(self)

		local s = 4

		self.chck:SetPos(s*0.5, s*0.5+1)
		self.chck:SetSize(self:GetTall()-s, self:GetTall()-s)

		self.lbl:MoveRightOf(self.chck, 5)
		self.lbl:CenterVertical()
		local w,h = self:GetParent():GetSize()
		self:SetSize(w-2,h)
	end

	pace.RegisterPanel(PANEL)
end
