local Keyboard, Radial, db = CPAPI.EventHandler(ConsolePortKeyboard, {'PLAYER_LOGOUT'}), ConsolePortRadial, ConsolePort:DB();
local L, _, env = db.Locale, ...;
local VALID_VEC_LEN, VALID_DSP_LEN = 0.5, 0.15;

---------------------------------------------------------------
-- Hardware events
---------------------------------------------------------------

function Keyboard:Left(x, y, len)
	self:ReflectStickPosition(x, y, len, len > VALID_VEC_LEN)

	local oldFocusSet = self.focusSet;
	self.focusSet = len > VALID_VEC_LEN and
		self.Registry[Radial:GetIndexForStickPosition(x, y, len, self.numSets)]

	local isNewFocusSet = oldFocusSet ~= self.focusSet;
	if oldFocusSet and isNewFocusSet then
		oldFocusSet:OnStickChanged(0, 0, 0, false)
		oldFocusSet:SetHighlight(false)
	end
	if isNewFocusSet and self.focusSet then
		self.focusSet:SetHighlight(true)
	end
end

function Keyboard:Right(x, y, len)
	if self.focusSet then
		self.focusSet:OnStickChanged(x, y, len, len > VALID_DSP_LEN)
	end
	if not self.inputLock and len > VALID_VEC_LEN then
		self.inputLock = true;
		self:Insert()
	elseif len <= VALID_VEC_LEN then
		self.inputLock = false;
	end
end

function Keyboard:OnGamePadStick(stick, x, y, len)
	if self[stick] then
		self[stick](self, x, y, len)
	end
	self:SetPropagateKeyboardInput(false)
end

function Keyboard:OnGamePadButtonDown(button)
	local callback = self.commands[button];
	if callback then
		callback(self, button)
		return self:SetPropagateKeyboardInput(false)
	end
	self:SetPropagateKeyboardInput(true)
end

function Keyboard:OnFocusChanged(frame)
	self:SetShown(frame)
	self.focusFrame = frame;
end

function Keyboard:SetState(state)
	if not self:IsShown() then return end
	if self.state == state then return end

	self.state = state;
	for widget in self:EnumerateActive() do
		widget:SetState(state)
	end
end

function Keyboard:OnShow()
	ConsolePortUIHandle:SetHintFocus(self)
	for _, data in ipairs(self.hints) do
		local text, key = unpack(data)
		if text and key then
			ConsolePortUIHandle:AddHint(key, text)
		end
	end
end

function Keyboard:OnHide()
	if ConsolePortUIHandle:IsHintFocus(self) then
		ConsolePortUIHandle:HideHintBar()
	end
	ConsolePortUIHandle:ClearHintsForFrame(self)
end

---------------------------------------------------------------
-- Input scripts
---------------------------------------------------------------
local utf8 = env.utf8;

function Keyboard:Insert()
	local key = self.focusSet and self.focusSet.focusKey;
	if key then
		self.focusFrame:Insert(key:GetText())
		key:Flash()
	end
end

function Keyboard:Erase()
	local pos = self.focusFrame:GetUTF8CursorPosition()
	if pos ~= 0 then
		local text, offset = (self.focusFrame:GetText())
		-- TODO: handle markers

		local newText =
			utf8.sub(text, 0, offset and pos - offset or pos - 1) .. -- prefix
			utf8.sub(text, pos + 1, utf8.len(text) - pos);           -- suffix
		self.focusFrame:SetText(newText)
		self.focusFrame:SetCursorPosition(utf8.pos(newText, offset and pos - offset or pos - 1))
	end
end

function Keyboard:Enter()
	ExecuteFrameScript(self.focusFrame, 'OnEnterPressed')
	if self.cachedFocusText then
		env.DictHandler:Update(env.Dictionary, self.cachedFocusText)
		self.cachedFocusText = nil;
	end
end

function Keyboard:Space()
	ExecuteFrameScript(self.focusFrame, 'OnSpacePressed')
	self.focusFrame:Insert(' ')
end

function Keyboard:Escape()
	ExecuteFrameScript(self.focusFrame, 'OnEscapePressed')
	self:OnFocusChanged(nil)
end

function Keyboard:MoveLeft()
	local text, pos = self.focusFrame:GetText(), self.focusFrame:GetUTF8CursorPosition()
	local marker = text:sub(pos - 4, pos):find('{rt%d}')
	self.focusFrame:SetCursorPosition(utf8.pos(text, marker and pos - 5 or pos - 1))
end

function Keyboard:MoveRight()
	local text, pos = self.focusFrame:GetText(), self.focusFrame:GetUTF8CursorPosition()
	local marker = text:sub(pos, pos + 5):find('{rt%d}')
	self.focusFrame:SetCursorPosition(utf8.pos(text, marker and pos + 5 or pos + 1))
end

function Keyboard:PrevWord()
	self.WordSuggester:SetDelta(-1)
end

function Keyboard:NextWord()
	self.WordSuggester:SetDelta(1)
end

function Keyboard:SpellCorrect()
	local word = self.WordSuggester:GetSuggestion()
	if word then
		local text = self.focusFrame:GetText()
		local _, startPos, endPos = utf8.getword(text, self.focusFrame:GetUTF8CursorPosition())
		if text and word and startPos and endPos then
			self.focusFrame:SetText(text:sub(0, startPos - 1) .. word .. text:sub(endPos + 1))
		end
	end
end

---------------------------------------------------------------
-- Data handling
---------------------------------------------------------------

function Keyboard:OnTextChanged(text, pos)
	local word = utf8.getword(text, pos);
	self.WordSuggester:OnWordChanged(word)
	self.cachedFocusText = text; -- cache for dictionary
end

function Keyboard:OnDataLoaded(...)
	self:OnVariableChanged()

	if not ConsolePort_KeyboardLayout then
		ConsolePort_KeyboardLayout = CopyTable(env.DefaultLayout)
	end
	if not ConsolePort_KeyboardMarkers then
		ConsolePort_KeyboardMarkers = CopyTable(env.DefaultMarkers)
	end
	if not ConsolePort_KeyboardDictionary then
		ConsolePort_KeyboardDictionary = env.DictHandler:Generate()
	end

	env.Layout     = ConsolePort_KeyboardLayout;
	env.Markers    = ConsolePort_KeyboardMarkers;
	env.Dictionary = ConsolePort_KeyboardDictionary;

	env:ToggleObserver(true)
	self:OnLayoutChanged()

	return CPAPI.BurnAfterReading;
end

function Keyboard:OnLayoutChanged()
	self:ReleaseAll()
	self.numSets = #ConsolePort_KeyboardLayout;
	for i, set in ipairs(ConsolePort_KeyboardLayout) do
		local widget, newObj = self:Acquire(i)
		if newObj then
			widget:OnLoad()
		end
		widget:SetPoint('CENTER', Radial:GetPointForIndex(i, self.numSets, self:GetSize()/1.75))
		widget:SetData(set)
		widget:Show()
	end
end

function Keyboard:OnVariableChanged()
	self.commands = {
		[db('keyboardEnterButton')]     = self.Enter;
		[db('keyboardEraseButton')]     = self.Erase;
		[db('keyboardSpaceButton')]     = self.Space;
		[db('keyboardEscapeButton')]    = self.Escape;
		[db('keyboardMoveLeftButton')]  = self.MoveLeft;
		[db('keyboardMoveRightButton')] = self.MoveRight;
		[db('keyboardNextWordButton')]  = self.NextWord;
		[db('keyboardPrevWordButton')]  = self.PrevWord;
		[db('keyboardAutoCorrButton')]  = self.SpellCorrect;
	};
	self.hints = {
		{L'Enter',   db('keyboardEnterButton')};
		{L'Erase',   db('keyboardEraseButton')};
		{L'Space',   db('keyboardSpaceButton')};
		{L'Escape',  db('keyboardEscapeButton')};
		{L'Insert',  db('keyboardAutoCorrButton')};
	};
	-- update dictionary settings
	env.DictMatchPattern  = db('keyboardDictPattern');
	env.DictMatchAlphabet = db('keyboardDictAlphabet');
	env.DictYieldRate     = db('keyboardDictYieldRate');
end

db:RegisterCallbacks(Keyboard.OnVariableChanged, Keyboard,
	'Settings/keyboardEraseButton',
	'Settings/keyboardEnterButton',
	'Settings/keyboardSpaceButton',
	'Settings/keyboardEscapeButton',
	'Settings/keyboardDictPattern',
	'Settings/keyboardDictAlphabet',
	'Settings/keyboardDictYieldRate'
);

function Keyboard:PLAYER_LOGOUT()
	env.DictHandler:Normalize(env.Dictionary)
end

---------------------------------------------------------------
-- Init
---------------------------------------------------------------
Keyboard:EnableGamePadStick(true)
Keyboard.Arrow:SetSize(50*0.71, 400*0.71)
Keyboard:SetScript('OnShow', Keyboard.OnShow)
Keyboard:SetScript('OnHide', Keyboard.OnHide)
Keyboard:SetScript('OnUpdate', Keyboard.OnUpdate)
Keyboard:SetScript('OnGamePadStick', Keyboard.OnGamePadStick)
Keyboard:SetScript('OnGamePadButtonDown', Keyboard.OnGamePadButtonDown)
CPFocusPoolMixin.CreateFramePool(Keyboard, 'PieMenu', 'ConsolePortKeyboardSet', env.CharsetMixin)