-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

--- Input UI Element Class.
-- The input element allows the user to enter text. It is a subclass of the @{BaseInput} class.
-- @classmod Input

local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local Input = TSM.Include("LibTSMClass").DefineClass("Input", TSM.UI.BaseInput)
UIElements.Register(Input)
TSM.UI.Input = Input
local private = {}
local PADDING_LEFT = 8
local PADDING_RIGHT = 8
local PADDING_TOP_BOTTOM = 4



-- ============================================================================
-- Public Class Methods
-- ============================================================================

function Input.__init(self)
	local frame = UIElements.CreateFrame(self, "EditBox")
	self._editBox = frame
	self.__super:__init(frame)

	self._hintText = UIElements.CreateFontString(self, frame)
	self._hintText:SetFont(Theme.GetFont("BODY_BODY3"):GetWowFont())
	self._hintText:SetJustifyH("LEFT")
	self._hintText:SetJustifyV("MIDDLE")
	self._hintText:SetPoint("TOPLEFT", PADDING_LEFT, 0)
	self._hintText:SetPoint("BOTTOMRIGHT", -PADDING_RIGHT, 0)

	self._icon = frame:CreateTexture(nil, "ARTWORK")
	self._icon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)

	self._clearBtn = CreateFrame("Button", nil, frame)
	self._clearBtn:SetAllPoints(self._icon)
	ScriptWrapper.Set(self._clearBtn, "OnClick", private.ClearBtnOnClick, self)

	self._subIcon = frame:CreateTexture(nil, "ARTWORK")
	self._subIcon:SetPoint("LEFT", PADDING_LEFT / 2, 0)
	TSM.UI.TexturePacks.SetTextureAndSize(self._subIcon, "iconPack.14x14/Subtract/Default")

	self._subBtn = CreateFrame("Button", nil, frame)
	self._subBtn:SetAllPoints(self._subIcon)
	ScriptWrapper.Set(self._subBtn, "OnClick", private.SubBtnOnClick, self)
	ScriptWrapper.SetPropagate(self._subBtn, "OnEnter")
	ScriptWrapper.SetPropagate(self._subBtn, "OnLeave")

	self._addIcon = frame:CreateTexture(nil, "ARTWORK")
	self._addIcon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)
	TSM.UI.TexturePacks.SetTextureAndSize(self._addIcon, "iconPack.14x14/Add/Default")

	self._addBtn = CreateFrame("Button", nil, frame)
	self._addBtn:SetAllPoints(self._addIcon)
	ScriptWrapper.Set(self._addBtn, "OnClick", private.AddBtnOnClick, self)
	ScriptWrapper.SetPropagate(self._addBtn, "OnEnter")
	ScriptWrapper.SetPropagate(self._addBtn, "OnLeave")

	ScriptWrapper.Set(frame, "OnEnter", private.OnEnter, self)
	ScriptWrapper.Set(frame, "OnLeave", private.OnLeave, self)

	local function ItemLinkedCallback(name, link)
		if self._allowItemInsert == nil or not self:IsVisible() or not self:HasFocus() then
			return
		end
		if self._allowItemInsert == true then
			self._editBox:Insert(link)
		else
			self._editBox:Insert(name)
		end
		return true
	end
	ItemLinked.RegisterCallback(ItemLinkedCallback, -1)

	self._clearEnabled = false
	self._subAddEnabled = false
	self._iconTexture = nil
	self._autoComplete = nil
	self._allowItemInsert = nil
	self._lostFocusOnButton = false
end

function Input.Release(self)
	self._clearEnabled = false
	self._subAddEnabled = false
	self._iconTexture = nil
	self._autoComplete = nil
	self._allowItemInsert = nil
	self._lostFocusOnButton = false
	self._hintText:SetText("")
	self.__super:Release()
end

--- Sets the horizontal justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn Input The input object
function Input.SetHintJustifyH(self, justifyH)
	assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
	self._hintText:SetJustifyH(justifyH)
	return self
end

--- Sets the vertical justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn Input The input object
function Input.SetHintJustifyV(self, justifyV)
	assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
	self._hintText:SetJustifyV(justifyV)
	return self
end

--- Sets the auto complete table.
-- @tparam Input self The input object
-- @tparam table tbl A list of strings to auto-complete to
-- @treturn Input The input object
function Input.SetAutoComplete(self, tbl)
	assert(type(tbl) == "table")
	self._autoComplete = tbl
	return self
end

--- Sets the hint text.
-- The hint text is shown when there's no other text in the input.
-- @tparam Input self The input object
-- @tparam string text The hint text
-- @treturn Input The input object
function Input.SetHintText(self, text)
	self._hintText:SetText(text)
	return self
end

--- Sets whether or not the clear button is enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the clear button is enabled
-- @treturn Input The input object
function Input.SetClearButtonEnabled(self, enabled)
	assert(type(enabled) == "boolean")
	assert(not self._subAddEnabled)
	self._clearEnabled = enabled
	return self
end

--- Sets whether or not the sub/add buttons are enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the sub/add buttons are enabled
-- @treturn Input The input object
function Input.SetSubAddEnabled(self, enabled)
	assert(type(enabled) == "boolean")
	assert(not self._clearEnabled and not self._iconTexture)
	self._subAddEnabled = enabled
	return self
end

--- Sets the icon texture.
-- @tparam Input self The input object
-- @tparam[opt=nil] string iconTexture The texture string to use for the icon texture
-- @treturn Input The input object
function Input.SetIconTexture(self, iconTexture)
	assert(iconTexture == nil or TSM.UI.TexturePacks.IsValid(iconTexture))
	assert(not self._subAddEnabled)
	self._iconTexture = iconTexture
	return self
end

--- Allows inserting an item into the input by linking it while the input has focus.
-- @tparam Input self The input object
-- @tparam[opt=false] boolean insertLink Insert the link instead of the item name
-- @treturn Input The input object
function Input.AllowItemInsert(self, insertLink)
	assert(insertLink == true or insertLink == false or insertLink == nil)
	self._allowItemInsert = insertLink or false
	return self
end

function Input.Draw(self)
	self.__super:Draw()
	self:_UpdateIconsForValue(self._value)
end



-- ============================================================================
-- Private Class Methods
-- ============================================================================

function Input._GetHintTextColor(self)
	local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor)
	if color:IsLight() then
		return self:_GetTextColor("+HOVER")
	else
		return self:_GetTextColor("-HOVER")
	end
end

function Input._UpdateIconsForValue(self, value)
	local frame = self:_GetBaseFrame()
	local leftPadding, rightPadding = PADDING_LEFT, PADDING_RIGHT

	-- set the hint text
	if value == "" and self._hintText:GetText() ~= "" then
		self._hintText:SetFont(Theme.GetFont(self._font):GetWowFont())
		self._hintText:SetTextColor(self:_GetHintTextColor():GetFractionalRGBA())
		self._hintText:Show()
	else
		self._hintText:Hide()
	end

	local showSubAdd = self._subAddEnabled and (frame:IsMouseOver() or frame:HasFocus())
	if showSubAdd then
		self._subIcon:Show()
		self._subBtn:Show()
		self._addIcon:Show()
		self._addBtn:Show()
	else
		self._subIcon:Hide()
		self._subBtn:Hide()
		self._addIcon:Hide()
		self._addBtn:Hide()
	end

	-- set the icon
	local iconTexture = nil
	if self._clearEnabled and value ~= "" then
		self._clearBtn:Show()
		iconTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.18x18/Close/Default", self:_GetTextColor())
	else
		self._clearBtn:Hide()
		iconTexture = not frame:HasFocus() and self._iconTexture and TSM.UI.TexturePacks.GetColoredKey(self._iconTexture, self:_GetTextColor()) or nil
	end
	if iconTexture then
		assert(not showSubAdd)
		self._icon:Show()
		TSM.UI.TexturePacks.SetTextureAndSize(self._icon, iconTexture)
		rightPadding = rightPadding + TSM.UI.TexturePacks.GetWidth(iconTexture)
	else
		self._icon:Hide()
	end
	frame:SetTextInsets(leftPadding, rightPadding, PADDING_TOP_BOTTOM, PADDING_TOP_BOTTOM)
	-- for some reason the text insets don't take effect right away, so on the next frame, we call GetTextInsets() which seems to fix things
	ScriptWrapper.Set(frame, "OnUpdate", private.OnUpdate, self)
end

function Input._OnTextChanged(self, value)
	self:_UpdateIconsForValue(value)
end

function Input._ShouldKeepFocus(self)
	if not IsMouseButtonDown("LeftButton") then
		return false
	end
	if self._clearBtn:IsVisible() and self._clearBtn:IsMouseOver() then
		return true
	elseif self._subBtn:IsVisible() and self._subBtn:IsMouseOver() then
		return true
	elseif self._addBtn:IsVisible() and self._addBtn:IsMouseOver() then
		return true
	else
		return false
	end
end



-- ============================================================================
-- Private Class Methods
-- ============================================================================

function Input._OnChar(self, c)
	self.__super:_OnChar(c)
	if not self._autoComplete then
		return
	end
	local frame = self:_GetBaseFrame()
	local text = frame:GetText()
	local match = nil
	for _, k in ipairs(self._autoComplete) do
		local start, ending = strfind(strlower(k), strlower(text), 1, true)
		if start == 1 and ending and ending == #text then
			match = k
			break
		end
	end
	if match and not IsControlKeyDown() then
		local compStart = #text
		frame:SetText(match)
		self:HighlightText(compStart, #match)
		frame:GetScript("OnTextChanged")(frame, true)
	end
end



-- ============================================================================
-- Local Script Handlers
-- ============================================================================

function private.OnUpdate(self)
	local frame = self:_GetBaseFrame()
	ScriptWrapper.Clear(frame, "OnUpdate")
	frame:GetTextInsets()
end

function private.ClearBtnOnClick(self)
	assert(self:_SetValueHelper(""))
	self._escValue = ""
	self._editBox:SetText(self._value)
	self:Draw()
end

function private.SubBtnOnClick(self)
	local minVal = self._validateContext and strsplit(":", self._validateContext)
	local value = tostring(max(tonumber(self:GetValue()) - (IsShiftKeyDown() and 10 or 1), minVal or -math.huge))
	if self:_SetValueHelper(value) then
		self._escValue = self._value
		self:_GetBaseFrame():SetText(value)
		self:_UpdateIconsForValue(value)
	end
end

function private.AddBtnOnClick(self)
	local _, maxVal = nil, nil
	if self._validateContext then
		_, maxVal = strsplit(":", self._validateContext)
	end
	local value = tostring(min(tonumber(self:GetValue()) + (IsShiftKeyDown() and 10 or 1), maxVal or math.huge))
	if self:_SetValueHelper(value) then
		self._escValue = self._value
		self:_GetBaseFrame():SetText(value)
		self:_UpdateIconsForValue(value)
	end
end

function private.OnEnter(self)
	self:_UpdateIconsForValue(self._value)
end

function private.OnLeave(self)
	self:_UpdateIconsForValue(self._value)
end
