-- VirtualKeyboard.lua --
-- Written by Kip Turner, copyright ROBLOX 2016 --


local CoreGui = game:GetService('CoreGui')
local RunService = game:GetService('RunService')
local UserInputService = game:GetService('UserInputService')
local GuiService = game:GetService('GuiService')
local HttpService = game:GetService('HttpService')
local ContextActionService = game:GetService('ContextActionService')
local PlayersService = game:GetService('Players')
local SoundService = game:GetService('SoundService')
local TextService = game:GetService('TextService')

local RobloxGui = CoreGui:WaitForChild("RobloxGui")
local Util = require(RobloxGui.Modules.Settings.Utility)

local BACKGROUND_OPACITY = 0.3
local NORMAL_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local HOVER_KEY_COLOR = Color3.new(49/255,49/255,49/255)
local PRESSED_KEY_COLOR = Color3.new(0,162/255,1)
local SET_KEY_COLOR = Color3.new(0,162/255,1)

local KEY_TEXT_COLOR = Color3.new(1,1,1)

local useSetTextFromInputSuccess, useSetTextFromInputValue = pcall(function() return settings():GetFFlag("UseSetTextFromInputForVrKeyboard") end)
local useSetTextFromInput = useSetTextFromInputSuccess and useSetTextFromInputValue
---------------------------------------- KEYBOARD LAYOUT --------------------------------------
local MINIMAL_KEYBOARD_LAYOUT = HttpService:JSONDecode([==[
[
  [
    {
      "a": 7,
      "w": 0.8
    },
    "*",
    "Q",
    "W",
    "E",
    "R",
    "T",
    "Y",
    "U",
    "I",
    "O",
    "P",
    {
      "w": 1.8
    },
    "Delete"
  ],
  [
    {
      "w": 1.6
    },
    "Caps",
    "A",
    "S",
    "D",
    "F",
    "G",
    "H",
    "J",
    "K",
    "L",
    "?",
    {
      "h": 2,
      "w2": 2.4,
      "h2": 1,
      "x2": -1.4,
      "y2": 1
    },
    "Enter"
  ],
  [
    {
      "w": 2.2
    },
    "Shift",
    "Z",
    "X",
    "C",
    "V",
    "B",
    "N",
    "M",
    "."
  ],
  [
    {
      "w": 2.2
    },
    "123/sym",
    {
      "w": 8
    },
    "",
    {
      "w": 2.4
    },
    "<Speaker>"
  ]
]
]==])

local MINIMAL_KEYBOARD_LAYOUT_SYMBOLS = HttpService:JSONDecode([==[
[
  [
    {
      "a": 7,
      "w": 0.8
    },
    "*",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "0",
    {
      "w": 1.8
    },
    "Delete"
  ],
  [
    {
      "w": 1.6
    },
    "!",
    "@",
    "#",
    "$",
    "%",
    "^",
    "&",
    "(",
    ")",
    "=",
    "?",
    {
      "h": 2,
      "w2": 2.4,
      "h2": 1,
      "x2": -1.4,
      "y2": 1
    },
    "Enter"
  ],
  [
    {
      "w": 1.2
    },
    "/",
    "-",
    "+",
    "_",
    ":",
    ";",
    "'",
    "\"",
    ",",
    "."
  ],
  [
    {
      "w": 2.2
    },
    "abc",
    {
      "w": 8
    },
    "",
    {
      "w": 2.4
    },
    "<Speaker>"
  ]
]
]==])


---------------------------------------- END KEYBOARD LAYOUT --------------------------------------


local VOICE_STATUS_CODE_ENUM = {}
do
	local STATUS_CODES =
	{
	    'ASR_STATUS_OK',
	    'ASR_STATUS_CANCELLED',
	    'ASR_STATUS_UNKNOWN',
	    'ASR_STATUS_INVALID_ARGUMENTS',
	    'ASR_STATUS_DEADLINE_EXCEEDED',
	    'ASR_STATUS_NOT_FOUND',
	    'ASR_STATUS_ALREADY_EXISTS',
	    'ASR_STATUS_PERMISSION_DENIED',
	    'ASR_STATUS_UNAUTHENTICATED',
	    'ASR_STATUS_RESOURCE_EXHAUSTED',
	    'ASR_STATUS_FAILED_PRECONDITION',
	    'ASR_STATUS_ABORTED',
	    'ASR_STATUS_OUT_OF_RANGE',
	    'ASR_STATUS_UNIMPLEMENTED',
	    'ASR_STATUS_INTERNAL',
	    'ASR_STATUS_UNAVAILABLE',
	    'ASR_STATUS_DATA_LOSS',
	    -- last official google response

	    -- Roblox statuses
	    'ASR_STATUS_NOT_ENABLED',
	    'ASR_STATUS_LOW_CONFIDENCE',
	    'ASR_STATUS_INVALID_JSON'
	};

	for i, code in pairs(STATUS_CODES) do
		VOICE_STATUS_CODE_ENUM[code] = i-1
	end
end

local function tokenizeString(str, tokenChar)
	local words = {}
	for word in string.gmatch(str, '([^' .. tokenChar .. ']+)') do
	    table.insert(words, word)
	end
	return words
end

local function ConvertFontSizeEnumToInt(fontSizeEnum)
	local result = string.match(fontSizeEnum.Name, '%d+')
	return (result and tostring(result)) or 12
end


-- RayPlaneIntersection

-- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm
local function RayPlaneIntersection(ray, planeNormal, pointOnPlane)
	planeNormal = planeNormal.unit
	ray = ray.Unit
	-- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane
	local Vd = planeNormal:Dot(ray.Direction)
	
	-- could fuzzy equals this a little bit to account for imprecision or very close angles to zero
	if Vd == 0 then -- parallel, no intersection
		return nil
	end

	local V0 = planeNormal:Dot(pointOnPlane - ray.Origin)
	local t = V0 / Vd

	if t < 0 then --plane is behind ray origin, and thus there is no intersection
		return nil
	end
	
	return ray.Origin + ray.Direction * t
end

function Clamp(low, high, input)
	return math.max(low, math.min(high, input))
end

-- No rotation as of yet
local function PointInGuiObject(object, x, y)
	local minPt = object.AbsolutePosition
	local maxPt = object.AbsolutePosition + object.AbsoluteSize
	if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then
		return true
	end
	return false
end

local function FindAncestorOfType(object, ancestorType)
	if not object then return nil end

	local parent = object.Parent
	if parent and  parent:IsA(ancestorType) then
		return parent
	end

	return FindAncestorOfType(parent, ancestorType)
end

local function ExtendedInstance(instance)
	local this = {}
	do
		local mt =
		{
			__index = function (t, k)
				return instance[k]
			end;

			__newindex = function (t, k, v)
				instance[k] = v
			end;
		}
		setmetatable(this, mt)
	end
	return this
end

local function IsVoiceToTextEnabled()
	local success, flagValue = pcall(function() return settings():GetFFlag("EnableVoiceRecording") end)
	return success and flagValue == true
end

local function CreateVRButton(instance)
	local newButton = ExtendedInstance(instance)

	rawset(newButton, "OnEnter", function(self)
	end)
	rawset(newButton, "OnLeave", function(self)
	end)
	rawset(newButton, "OnDown", function(self)
	end)
	rawset(newButton, "OnUp", function(self)
	end)
	rawset(newButton, "ContainsPoint", function(self, x, y)
		return PointInGuiObject(instance, x, y)
	end)
	rawset(newButton, "Update", function(self)
	end)

	return newButton
end

local selectionRing = Util:Create'ImageLabel'
{
	Name = 'SelectionRing';
	Size = UDim2.new(1, -6, 1, -6);
	Position = UDim2.new(0, 4, 0, 3);
	Image = 'rbxasset://textures/ui/menu/buttonHover.png';
	ScaleType = Enum.ScaleType.Slice;
	SliceCenter = Rect.new(94/2, 94/2, 94/2, 94/2);
	BackgroundTransparency = 1;
}

local KEY_ICONS =
{
	["<Speaker>"] = {Asset = "rbxasset://textures/ui/Keyboard/mic_icon.png", AspectRatio = 0.615};
}

local function CreateKeyboardKey(keyboard, layoutData, keyData)
	local isSpecialShapeKey = layoutData['width2'] and layoutData['height2'] and layoutData['x2'] and layoutData['y2']

	local newKeyElement = Util:Create'ImageButton'
	{
		Name = keyData[1];
		Position = UDim2.new(layoutData['x'], 0, layoutData['y'], 0);
		Size = UDim2.new(layoutData['width'], 0, layoutData['height'], 0);
		BorderSizePixel = 0;
		Image = "";
		BackgroundTransparency = 1;
		ZIndex = 1;
	}
	local keyText = Util:Create'TextLabel'
	{
		Name = "KeyText";
		Text = keyData[#keyData];
		Position = UDim2.new(0, -10, 0, -10);
		Size = UDim2.new(1, 0, 1, 0);
		Font = Enum.Font.SourceSansBold;
		FontSize = Enum.FontSize.Size96;
		TextColor3 = KEY_TEXT_COLOR;
		BackgroundTransparency = 1;
		Selectable = true;
		ZIndex = 2;
		Parent = newKeyElement;
	}
	local backgroundImage = Util:Create'Frame'
	{
		Name = 'KeyBackground';
		Size = UDim2.new(1,-10,1,-10);
		Position = UDim2.new(0,-5,0,-5);
		BackgroundColor3 = NORMAL_KEY_COLOR;
		BackgroundTransparency = BACKGROUND_OPACITY;
		BorderSizePixel = 0;
		Parent = newKeyElement;
	}
	
	local selectionObject = Util:Create'ImageLabel'
	{
		Name = 'SelectionObject';
		Size = UDim2.new(1,0,1,0);
		BackgroundTransparency = 1;
		Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
		ImageTransparency = 0;
		ScaleType = Enum.ScaleType.Slice;
		SliceCenter = Rect.new(12,12,52,52);
		BorderSizePixel = 0;
	}

	newKeyElement.SelectionImageObject = Util:Create'ImageLabel'
	{
		Visible = false;
	}

 	-- Special silly enter key nonsense
	local secondBackgroundImage = nil
	local specialSelectionObject, specialSelectionObject2, specialSelectionObject3 = nil, nil, nil
	if isSpecialShapeKey then
		secondBackgroundImage = Util:Create'ImageButton'
		{
			Name = 'KeyBackground';
			Position = UDim2.new(layoutData['x2'] / layoutData['width'], -5, layoutData['y2'] / layoutData['height'], -5);
			Size = UDim2.new(layoutData['width2'] / layoutData['width'], 0, layoutData['height2'] / layoutData['height'], -10);
			BackgroundColor3 = NORMAL_KEY_COLOR;
			BackgroundTransparency = BACKGROUND_OPACITY;
			BorderSizePixel = 0;
			AutoButtonColor = false;
			Parent = newKeyElement;
		}
		if layoutData['x2'] <= 0 then
			keyText.Size = secondBackgroundImage.Size - UDim2.new(0,10,0,0)
			keyText.Position = secondBackgroundImage.Position
			secondBackgroundImage.Size = secondBackgroundImage.Size - UDim2.new(1,0,0,0)
		end

		do
			specialSelectionObject = Util:Create'Frame'
			{
				Name = 'SpecialSelectionObject';
				Size = UDim2.new(1,0,0.5,0);
				Position = UDim2.new(0,0,0.5,0);
				BackgroundTransparency = 1;
				ClipsDescendants = true;
				Util:Create'ImageLabel'
				{
					Name = 'Borders';
					Position = UDim2.new(-1,0,-1,0);
					Size = UDim2.new(2,0,2,0);
					BackgroundTransparency = 1;
					Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png";
					ImageTransparency = 0;
					ScaleType = Enum.ScaleType.Slice;
					SliceCenter = Rect.new(12,12,52,52);
				};
			}
			specialSelectionObject2 = specialSelectionObject:Clone()
			specialSelectionObject2.Size = UDim2.new(1,0,0.5,5)
			specialSelectionObject2.Position = UDim2.new(0,0,0,0)
			specialSelectionObject2.Borders.Size = UDim2.new(1,0,1,30)
			specialSelectionObject2.Borders.Position = UDim2.new(0,0,0,0)

			specialSelectionObject3 = specialSelectionObject:Clone()
			specialSelectionObject3.Size = UDim2.new(1,5,1,0)
			specialSelectionObject3.Position = UDim2.new(0,0,0,0)
			specialSelectionObject3.Borders.Size = UDim2.new(1,30,1,0)
			specialSelectionObject3.Borders.Position = UDim2.new(0,0,0,0)
		end
		-- End of nonsense
	end

	local newKey = CreateVRButton(newKeyElement)

	local hovering = false
	local pressed = false
	local isAlpha = #keyData == 1 and type(keyData[1]) == 'string' and #keyData[1] == 1 and
	                string.byte(keyData[1]) >= string.byte("A") and string.byte(keyData[1]) <= string.byte("z")

	local icon = nil
	if keyData[1] and KEY_ICONS[keyData[1]] then
		keyText.Visible = false
		icon = Util:Create'ImageLabel'
		{
			Name = 'KeyIcon';
			Size = UDim2.new(KEY_ICONS[keyData[1]].AspectRatio, -20, 1, -20);
			SizeConstraint = Enum.SizeConstraint.RelativeYY;
			BackgroundTransparency = 1;
			Image = KEY_ICONS[keyData[1]].Asset;
			Parent = backgroundImage;
		}

		local function onChanged(prop)
			if prop == 'AbsoluteSize' then
				icon.Position = UDim2.new(0.5,-icon.AbsoluteSize.X/2,0.5,-icon.AbsoluteSize.Y/2);
			end
		end
		icon.Changed:connect(onChanged)
		onChanged('AbsoluteSize')
	end

	local function isEnabled()
		if newKey:GetCurrentKeyValue() == "<Speaker>" then
			return IsVoiceToTextEnabled()
		end
		return true
	end

	local function onClicked()
		local keyValue = nil
		local currentKeySetting = newKey:GetCurrentKeyValue()

		if currentKeySetting == 'Shift' then
			keyboard:SetShift(not keyboard:GetShift())
		elseif currentKeySetting == 'Caps' then
			keyboard:SetCaps(not keyboard:GetCaps())
		elseif currentKeySetting == 'Enter' then
			keyboard:SubmitText(true, true)
		elseif currentKeySetting == 'Delete' then
			keyboard:BackspaceAtCursor()
		elseif currentKeySetting == "123/sym" then
			keyboard:SetCurrentKeyset(2)
		elseif currentKeySetting == "abc" then
			keyboard:SetCurrentKeyset(1)
		elseif currentKeySetting == "<Speaker>" then
			keyboard:SetVoiceMode(true)
		elseif currentKeySetting == 'Tab' then
			keyValue = '\t'
		else
			keyValue = currentKeySetting
		end

		if keyValue ~= nil then
			keyboard:SubmitCharacter(keyValue, isAlpha)
		end
	end

	local function setKeyColor(newColor, hovering)
		backgroundImage.BackgroundColor3 = newColor
		if secondBackgroundImage then
			secondBackgroundImage.BackgroundColor3 = newColor
		end
		if isSpecialShapeKey then
			specialSelectionObject.Parent = hovering and backgroundImage or nil
			specialSelectionObject2.Parent = hovering and backgroundImage or nil
			specialSelectionObject3.Parent = hovering and secondBackgroundImage or nil
		else
			selectionObject.Parent = hovering and backgroundImage or nil
		end
	end

	local function update()
		local currentKey = newKey:GetCurrentKeyValue()

		if pressed then
			setKeyColor(PRESSED_KEY_COLOR, false)
		elseif hovering then
			setKeyColor(HOVER_KEY_COLOR, true)
		elseif currentKey == 'Caps' and keyboard:GetCaps() then
			setKeyColor(SET_KEY_COLOR, false)
		elseif currentKey == 'Shift' and keyboard:GetShift() then
			setKeyColor(SET_KEY_COLOR, false)
		elseif currentKey == 'abc' then
			setKeyColor(SET_KEY_COLOR, false)
		else
			setKeyColor(NORMAL_KEY_COLOR, false)
		end

		if icon then
			icon.ImageTransparency = isEnabled() and 0 or 0.5
		end

		keyText.Text = newKey:GetCurrentKeyValue()
	end

	local hoveringGuiElements = {}

	rawset(newKey, "OnEnter", function(self)
		hovering = true
		update()
	end)
	rawset(newKey, "OnLeave", function(self)
		if not next(hoveringGuiElements) then
			hovering = false
			pressed = false
			update()
		end
	end)
	rawset(newKey, "OnDown", function(self)
		pressed = true
		update()
		-- Fire the onclick when pressing down on the button;
		-- pressing down and up on the same button is difficult
		-- in VR because your head is constantly moving around
		onClicked()
	end)
	rawset(newKey, "OnUp", function(self)
		pressed = false
		update()
	end)
	rawset(newKey, "GetCurrentKeyValue", function(self)
		local shiftEnabled = keyboard:GetShift()
		local capsEnabled = keyboard:GetCaps()

		if isAlpha then
			if capsEnabled and shiftEnabled then
				return string.lower(keyData[#keyData])
			elseif capsEnabled or shiftEnabled then
				return keyData[1]
			else
				return string.lower(keyData[#keyData])
			end
		end

		if shiftEnabled then
			return keyData[1]
		end

		return keyData[#keyData]
	end)
	rawset(newKey, "ContainsPoint", function(self, x, y)
		return PointInGuiObject(backgroundImage, x, y) or
		       (secondBackgroundImage and PointInGuiObject(secondBackgroundImage, x, y))
	end)
	rawset(newKey, "Update", function(self)
		update()
	end)
	rawset(newKey, "GetInstance", function(self)
		return newKeyElement
	end)

	newKeyElement.MouseButton1Down:connect(function() newKey:OnDown() end)
	newKeyElement.MouseButton1Up:connect(function() newKey:OnUp() end)
	newKeyElement.SelectionGained:connect(function() hoveringGuiElements[newKeyElement] = true newKey:OnEnter() end)
	newKeyElement.SelectionLost:connect(function() hoveringGuiElements[newKeyElement] = nil newKey:OnLeave() end)
	-- For the time being, we will simulate onClick events in the OnDown() event
	-- newKeyElement.MouseButton1Click:connect(function() onClicked() end)
	if secondBackgroundImage then
		-- For the time being, we will simulate onClick events in the OnDown() event
		-- secondBackgroundImage.MouseButton1Click:connect(onClicked)
		secondBackgroundImage.MouseButton1Down:connect(function() newKey:OnDown() end)
		secondBackgroundImage.MouseButton1Up:connect(function() newKey:OnUp() end)
		secondBackgroundImage.SelectionGained:connect(function()
			hoveringGuiElements[secondBackgroundImage] = true
			newKey:OnEnter()
		end)
		secondBackgroundImage.SelectionLost:connect(function()
			hoveringGuiElements[secondBackgroundImage] = nil
			newKey:OnLeave()
		end)
	end

	update()

	return newKey
end

local function CreateBaseVoiceState()
	local this = {}
	this.Name = "Base"

	function this:TransitionFrom()
	end
	function this:TransitionTo()
	end

	return this
end

local function CreateListeningVoiceState()
	local this = CreateBaseVoiceState()

	this.Name = "Listening"

	function this:TransitionTo()
		pcall(function() SoundService:BeginRecording() end)
	end

	return this
end

local function CreateProcessingVoiceState()
	local this = CreateBaseVoiceState()

	this.Name = "Processing"

	local finished = false
	local result = nil

	function this:TransitionTo()
		coroutine.wrap(function()
			pcall(function() result = SoundService:EndRecording() end)
			finished = true
		end)()
	end

	function this:GetResultAsync()
		while not finished do
			wait()
		end
		return result
	end

	return this
end

local function CreateWaitingVoiceState()
	local this = CreateBaseVoiceState()

	this.Name = "Waiting"

	return this
end

local VoiceTransitions = {Listening = {Processing = true}, Processing = {Waiting = true}, Waiting = {Listening = true}}

local VoiceToTextFSM = {}
do
	VoiceToTextFSM.CurrentState = CreateWaitingVoiceState()

	local stateTransitionedSignal = Instance.new('BindableEvent')

	function VoiceToTextFSM:TransitionState(newState)
		-- If it is a new state then lets cleanup and activate it
		if VoiceTransitions[self.CurrentState.Name][newState.Name] then
			self.CurrentState:TransitionFrom()
			self.CurrentState = newState
			self.CurrentState:TransitionTo()
			stateTransitionedSignal:Fire(self.CurrentState)
			return true
		end
		return false
	end

	function VoiceToTextFSM:GetCurrentState()
		return self.CurrentState
	end

	VoiceToTextFSM.StateTransitionedEvent = stateTransitionedSignal.Event
end



local function ConstructKeyboardUI(keyboardLayoutDefinitions)
	local Panel3D = require(RobloxGui.Modules.VR.Panel3D)
	local panel = Panel3D.Get("Keyboard")
	panel:SetVisible(false)

	-- DEBUG setting
	-- panel:GetGUI().Parent = workspace


	local buttons = {}

	local keyboardContainer = Util:Create'Frame'
	{
		Name = 'VirtualKeyboard';
		Size = UDim2.new(1, 0, 1, 0);
		Position = UDim2.new(0, 0, 0, 0);
		BackgroundTransparency = 1;
		Active = true;
		Visible = false;
	};

	local textEntryBackground = Util:Create'ImageLabel'
	{
		Name = 'TextEntryBackground';
		Size = UDim2.new(0.5,0,0.125,0);
		Position = UDim2.new(0.25,0,0,0);
		Image = "";
		BackgroundTransparency = 0.5;
		BackgroundColor3 = Color3.new(31/255,31/255,31/255);
		BorderSizePixel = 0;
		ClipsDescendants = true;
		Parent = keyboardContainer;
	}
		local textfieldBackground = Util:Create'Frame'
		{
			Name = 'TextfieldBackground';
			Position = UDim2.new(0,2,0,2);
			Size = UDim2.new(1, -4, 1, -4);
			BackgroundTransparency = 0;
			BackgroundColor3 = Color3.new(209/255,216/255,221/255);
			BorderSizePixel = 0;
			Visible = true;
			Parent = textEntryBackground;
		};
			local textEntryField = Util:Create'TextButton'
			{
				Name = "TextEntryField";
				Text = "";
				Position = UDim2.new(0,4,0,4);
				Size = UDim2.new(1, -8, 1, -8);
				Font = Enum.Font.SourceSans;
				FontSize = Enum.FontSize.Size60;
				TextXAlignment = Enum.TextXAlignment.Left;
				BackgroundTransparency = 1;
				BorderSizePixel = 0;
				Parent = textfieldBackground;
			}
				local textfieldCursor = Util:Create'Frame'
				{
					Name = 'TextfieldCursor';
					Size = UDim2.new(0, 5, 0.9, 0);
					Position = UDim2.new(0, 0, 0.05, 0);
					BackgroundTransparency = 0;
					BackgroundColor3 = SET_KEY_COLOR;
					BorderSizePixel = 0;
					Visible = true;
					ZIndex = 2;
					Parent = textEntryField;
				};

	local closeButtonElement = Util:Create'ImageButton'
	{
		Name = 'CloseButton';
		Size = UDim2.new(0.075,-10,0.198,-10);
		Position = UDim2.new(0,-5,0,-35);
		Image = "rbxasset://textures/ui/Keyboard/close_button_background.png";
		BackgroundTransparency = 1;
		AutoButtonColor = false;
		Parent = keyboardContainer;
	}
	do
		closeButtonElement.SelectionImageObject = Util:Create'ImageLabel'
		{
			Name = 'Selection';
			Size = UDim2.new(0.9,0,0.9,0);
			Position = UDim2.new(0.05,0,0.05,0);
			Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png";
			BackgroundTransparency = 1;
		}
		Util:Create'ImageLabel'
		{
			Name = 'Icon';
			Size = UDim2.new(0.5,0,0.5,0);
			Position = UDim2.new(0.25,0,0.25,0);
			Image = "rbxasset://textures/ui/Keyboard/close_button_icon.png";
			BackgroundTransparency = 1;
			Parent = closeButtonElement;
		}
	end
	local closeButton = CreateVRButton(closeButtonElement)
	table.insert(buttons, closeButton)

	local voiceRecognitionContainer = Util:Create'Frame'
	{
		Name = 'VoiceRecognitionContainer';
		Size = UDim2.new(1, 0, 0.85, 0);
		Position = UDim2.new(0, 0, 0.15, 0);
		BackgroundTransparency = 1;
		Active = true;
		Visible = false;
		Parent = keyboardContainer;
	};
	do
		local voiceRecognitionBackground1 = Util:Create'Frame'
		{
			Name = 'voiceRecognitionBackground1';
			Size = UDim2.new(1, 0, 0.75, 0);
			Position = UDim2.new(0, 0, 0, 0);
			BackgroundColor3 = NORMAL_KEY_COLOR;
			BackgroundTransparency = BACKGROUND_OPACITY;
			BorderSizePixel = 0;
			Active = true;
			Parent = voiceRecognitionContainer;
		};
		local voiceRecognitionBackground2 = voiceRecognitionBackground1:Clone()
		voiceRecognitionBackground2.Size = UDim2.new(1 - 0.2, 0, 0.25, 0)
		voiceRecognitionBackground2.Position = UDim2.new(0, 0, 0.75, 0)
		voiceRecognitionBackground2.Parent = voiceRecognitionContainer
	end

	local voiceDoneButton = CreateVRButton(Util:Create'TextButton'
	{
		Name = 'DoneButton';
		Size = UDim2.new(0.2, -5, 0.25, -5);
		Position = UDim2.new(1 - 0.2, 5, 0.75, 5);
		Text = "Done";
		BackgroundColor3 = SET_KEY_COLOR;
		Font = Enum.Font.SourceSansBold;
		FontSize = Enum.FontSize.Size96;
		TextColor3 = KEY_TEXT_COLOR;
		BackgroundTransparency = 0;
		AutoButtonColor = false;
		BorderSizePixel = 0;
		Parent = voiceRecognitionContainer;
	})
	table.insert(buttons, voiceDoneButton)

	local voiceProcessingStatus = Util:Create'TextLabel'
	{
		Name = 'VoiceProcessingStatus';
		Size = UDim2.new(0, 0, 0, 0);
		Position = UDim2.new(0.5, 0, 0.33, 0);
		Text = "";
		Font = Enum.Font.SourceSansBold;
		FontSize = Enum.FontSize.Size96;
		TextColor3 = KEY_TEXT_COLOR;
		BackgroundTransparency = 1;
		BorderSizePixel = 0;
		Parent = voiceRecognitionContainer;
	}

	local function CreateVoiceVisualizerWidget()
		local this = {}

		local bars = {}

		local numOfBars = 50
		local numOfWaves = 4
		local waveSpeed = 2.5

		local container = Util:Create'Frame'
		{
			Name = 'VoiceVisualizerContainer';
			Size = UDim2.new(1, 0, 1, 0);
			BackgroundTransparency = 1;
		}
		this.Container = container

		for i = 1, numOfBars do
			local bar = Util:Create'Frame'
			{
				Name = 'Bar';
				Size = UDim2.new(1/numOfBars, -4, 1, 0);
				Position = UDim2.new(i/numOfBars, 0, 0, 0);
				BackgroundTransparency = 0;
				BackgroundColor3 = KEY_TEXT_COLOR;
				Parent = container;
			}
			table.insert(bars, bar)
		end

		function this:StartAnimation()
			RunService:UnbindFromRenderStep("VoiceVisualizerWidget")
			RunService:BindToRenderStep("VoiceVisualizerWidget", Enum.RenderPriority.First.Value,
				function()
					local movementPerBar = (numOfWaves*2*math.pi) / numOfBars
					for i, bar in pairs(bars) do
						local height = math.abs(math.sin(tick() * waveSpeed + i * movementPerBar)) + math.abs(math.cos(tick() * waveSpeed + i * movementPerBar))
						height = ((height / 2) - 0.3) * (1/(1-0.3))
						bar.Size = UDim2.new(1/numOfBars, -4, height, 0)
						bar.Position = UDim2.new(i/numOfBars, 0, (1-height) / 2, 0)
					end
				end)
		end

		function this:StopAnimation()
			RunService:UnbindFromRenderStep("VoiceVisualizerWidget")
		end

		return this
	end

	local voiceVisualizer = CreateVoiceVisualizerWidget()
	voiceVisualizer.Container.Parent = voiceRecognitionContainer
	voiceVisualizer.Container.Size = UDim2.new(0.5,0,0.4,0)
	voiceVisualizer.Container.Position = UDim2.new(0.25,0,0.4,0)

	local newKeyboard = ExtendedInstance(keyboardContainer)

	local keyboardOptions = nil
	local keysets = {}

	local capsLockEnabled = false
	local shiftEnabled = false

	local textfieldCursorPosition = 0

	local openedEvent = Instance.new('BindableEvent')
	local closedEvent = Instance.new('BindableEvent')
	local opened = false

	local function SetTextFieldCursorPosition(newPosition)
		textfieldCursorPosition = Clamp(0, #textEntryField.Text, newPosition)
		if not textEntryField.TextFits then
			textfieldCursorPosition = #textEntryField.Text
		end

		local textSize = TextService:GetTextSize(
			string.sub(textEntryField.Text, 1, textfieldCursorPosition),
			ConvertFontSizeEnumToInt(textEntryField.FontSize),
			textEntryField.Font,
			textEntryField.AbsoluteSize)
		textfieldCursor.Position = UDim2.new(0, textSize.x, textfieldCursor.Position.Y.Scale, textfieldCursor.Position.Y.Offset)
	end

	local function UpdateTextEntryFieldText(newText)
		local textSet = false
		if useSetTextFromInput then
			textSet = pcall(function() textEntryField:SetTextFromInput(newText) end)
		end

		if not textSet then
			textEntryField.Text = newText
		end

		SetTextFieldCursorPosition(textfieldCursorPosition)
	end

	local buffer = ""
	local function getBufferText()
		if keyboardOptions and keyboardOptions.TextBox then
			return keyboardOptions.TextBox.Text
		end
		return buffer
	end
	local function setBufferText(newBufferText)
		if keyboardOptions and keyboardOptions.TextBox then
			keyboardOptions.TextBox.Text = newBufferText
		elseif buffer ~= newBufferText then
			buffer = newBufferText
			UpdateTextEntryFieldText(buffer)
		end
	end

	local function calculateTextCursorPosition(x, y)
		x = x - textEntryField.AbsolutePosition.x
		y = y - textEntryField.AbsolutePosition.y

		for i = 1, #textEntryField.Text do
			local textSize = TextService:GetTextSize(
				string.sub(textEntryField.Text, 1, i),
				ConvertFontSizeEnumToInt(textEntryField.FontSize),
				textEntryField.Font,
				textEntryField.AbsoluteSize)
			if textSize.x > x then
				return i - 1
			end
		end

		return #textEntryField.Text
	end

	local currentKeyset = nil

	rawset(newKeyboard, "OpenedEvent",  openedEvent.Event)
	rawset(newKeyboard, "ClosedEvent",  closedEvent.Event)

	rawset(newKeyboard, "GetCurrentKeyset", function(self)
		return keysets[currentKeyset]
	end)

	rawset(newKeyboard, "SetCurrentKeyset", function(self, newKeyset)
		if newKeyset ~= currentKeyset and keysets[newKeyset] ~= nil then
			if keysets[currentKeyset] and keysets[currentKeyset].container then
				keysets[currentKeyset].container.Visible = false
			end

			currentKeyset = newKeyset

			if keysets[currentKeyset] and keysets[currentKeyset].container then
				keysets[currentKeyset].container.Visible = true
			end
		end
	end)

	rawset(newKeyboard, "SetVoiceMode", function(self, inVoiceMode)
		inVoiceMode = inVoiceMode and IsVoiceToTextEnabled()

		local currentKeysetObject = self:GetCurrentKeyset()
		if currentKeysetObject and currentKeysetObject.container then
			currentKeysetObject.container.Visible = not inVoiceMode
		end

		voiceRecognitionContainer.Visible = inVoiceMode

		if inVoiceMode then
			VoiceToTextFSM:TransitionState(CreateListeningVoiceState())
		end
	end)

	rawset(newKeyboard, "GetCaps", function(self)
		return capsLockEnabled
	end)

	rawset(newKeyboard, "SetCaps", function(self, newCaps)
		capsLockEnabled = newCaps
		for _, key in pairs(self:GetCurrentKeyset().keys) do
			key:Update()
		end
	end)

	rawset(newKeyboard, "GetShift", function(self)
		return shiftEnabled
	end)

	rawset(newKeyboard, "SetShift", function(self, newShift)
		shiftEnabled = newShift
		for _, key in pairs(self:GetCurrentKeyset().keys) do
			key:Update()
		end
	end)

	local ignoreFocusedLost = false

	local textChangedConn = nil
	local textBoxFocusLostConn = nil
	local panelClosedConn = nil

	local function disconnectKeyboardEvents()
		if textChangedConn then textChangedConn:disconnect() end
		textChangedConn = nil
		if textBoxFocusLostConn then textBoxFocusLostConn:disconnect() end
		textBoxFocusLostConn = nil
		if panelClosedConn then panelClosedConn:disconnect() end
		panelClosedConn = nil
	end

	rawset(newKeyboard, "Open", function(self, options)
		if opened then return end
		opened = true

		keyboardOptions = options

		self:SetCurrentKeyset(1)
		self:SetVoiceMode(false)
		keyboardContainer.Visible = true

		panel:ResizeStuds(5.9, 2.25, 320)

		local localCF = CFrame.new()

		disconnectKeyboardEvents()
		if options.TextBox then
			textChangedConn = options.TextBox.Changed:connect(function(prop)
				if prop == 'Text' then
					UpdateTextEntryFieldText(options.TextBox.Text)
				end
			end)
			textBoxFocusLostConn = options.TextBox.FocusLost:connect(function(submitted)
				if not ignoreFocusedLost then
					self:Close(submitted)
				end
			end)
			if options.TextBox.ClearTextOnFocus then
				setBufferText("")
			else
				UpdateTextEntryFieldText(options.TextBox.Text)
			end

			-- Find panel for 2d ui?
			local textboxPanel = Panel3D.FindContainerOf(options.TextBox)
			if textboxPanel then
				panelClosedConn = Panel3D.OnPanelClosed.Event:connect(function(closedPanelName)
					if closedPanelName == textboxPanel.name then
						self:Close(false)
					end
				end)

				-- Attach to it if it's linked to our keyboard panel
				if textboxPanel.linkedTo == panel then
					local panelCF = textboxPanel.localCF
					localCF = panelCF * CFrame.new(0, (-textboxPanel.height / 2) - 0.5, 0) * 
										CFrame.Angles(math.rad(30), 0, 0) * 
										CFrame.new(0, (-panel.height / 2) - 0.5, 0)
				else
					--Otherwise, best-guess where it should go based on the user's head.
					local headForwardCF = Panel3D.GetHeadLookXZ(true)
					localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, 5)
				end
			else -- no panel!
				local headForwardCF = Panel3D.GetHeadLookXZ(true)
				localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, 5)
			end
		else
			setBufferText("")
		end

		ContextActionService:BindCoreAction("VirtualKeyboardControllerInput",
			function(actionName, inputState, inputObject)
				if inputState == Enum.UserInputState.End then
					if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then
						SetTextFieldCursorPosition(textfieldCursorPosition - 1)
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then
						SetTextFieldCursorPosition(textfieldCursorPosition + 1)
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonX then
						self:BackspaceAtCursor()
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonY then
						self:SubmitCharacter(" ", false)
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonL2 then
						if currentKeyset then
							-- Go to the next keyset
							self:SetCurrentKeyset((currentKeyset % #keysets) + 1)
						end
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonL3 then
						self:SetCaps(not self:GetCaps())
					elseif inputObject.KeyCode == Enum.KeyCode.ButtonB then
						self:Close(false)
					end
				end
			end,
			false,
			Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonL3, Enum.KeyCode.ButtonX, Enum.KeyCode.ButtonY, Enum.KeyCode.ButtonR2, Enum.KeyCode.ButtonB)

		self.Parent = panel:GetGUI()

		panel:SetType(Panel3D.Type.Fixed, { CFrame = localCF })
		panel:SetCanFade(false)
		panel:SetVisible(true, true)
		panel:ForceShowUntilLookedAt()

		Panel3D.Get("Topbar3D"):SetVisible(false)

		function panel:OnUpdate()
		end

		openedEvent:Fire()
	end)

	rawset(newKeyboard, "Close", function(self, submit)
		submit = (submit == true)

		if not opened then return end
		opened = false

		disconnectKeyboardEvents()

		ContextActionService:UnbindCoreAction("VirtualKeyboardControllerInput")
		-- Clean-up
		panel:OnMouseLeave()
		panel:SetVisible(false, true)
		keyboardContainer.Visible = false

		Panel3D.Get("Topbar3D"):SetVisible(true)
		
		self:SubmitText(submit, false)
		closedEvent:Fire()
	end)

	rawset(newKeyboard, "SubmitText", function(self, submit, keepKeyboardOpen)
		local keyboardTextbox = keyboardOptions and keyboardOptions.TextBox
		if keyboardTextbox then
			if submit then
				keyboardTextbox.Text = getBufferText()
			end
			-- Only keep text boxes open for coreguis, such as chat
			local textboxPanel = Panel3D.FindContainerOf(keyboardTextbox)
			local reopenKeyboard = keepKeyboardOpen and textboxPanel and textboxPanel.linkedTo == panel

			if reopenKeyboard then
				ignoreFocusedLost = true
			end

			keyboardTextbox:ReleaseFocus(submit)

			if reopenKeyboard then
				keyboardTextbox:CaptureFocus()
				ignoreFocusedLost = false
			end
		end
	end)

	rawset(newKeyboard, "GetCurrentOptions", function(self)
		return keyboardOptions
	end)

	rawset(newKeyboard, "BackspaceAtCursor", function(self)
		if textfieldCursorPosition >= 1 then
			local bufferText = getBufferText()
			local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition - 1) .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText)
			local newCursorPosition = textfieldCursorPosition - 1
			setBufferText(newBufferText)
			SetTextFieldCursorPosition(newCursorPosition)
		end
	end)

	rawset(newKeyboard, "SubmitCharacter", function(self, character, isAnAlphaKey)
		local bufferText = getBufferText()
		local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition) .. character .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText)
		setBufferText(newBufferText)
		SetTextFieldCursorPosition(textfieldCursorPosition + #character)

		if isAnAlphaKey and self:GetShift() then
			self:SetShift(false)
		end
	end)

	do -- Parse input definition
		for _, keyboardKeyset in pairs(keyboardLayoutDefinitions) do
			local keys = {}
			local keyboardSizeConstrainer = Util:Create'Frame'
			{
				Name = 'KeyboardSizeConstrainer';
				Size = UDim2.new(1, 0, 1, -20);
				Position = UDim2.new(0, 0, 0, 20);
				BackgroundTransparency = 1;
				Parent = keyboardContainer;
			};

			local maxWidth = 0
			local maxHeight = 0
			local y = 0
			for rowNum, rowData in pairs(keyboardKeyset) do
				local x = 0
				local width = 1
				local height = 1
				local width2, height2, x2, y2;
				for columnNum, columnData in pairs(rowData) do
					if type(columnData) == 'table' then
						if columnData['w'] then width = columnData['w'] end
						if columnData['h'] then height = columnData['h'] end
						if columnData['x'] then x = x + columnData['x'] end
						if columnData['y'] then y = y + columnData['y'] end
						if columnData['x2'] then x2 = columnData['x2'] end
						if columnData['y2'] then y2 = columnData['y2'] end
						if columnData['w2'] then width2 = columnData['w2'] end
						if columnData['h2'] then height2 = columnData['h2'] end
					elseif type(columnData) == 'string' then
						if columnData == "" then
							columnData = " "
						end
						-- put key
						local key = CreateKeyboardKey(
							newKeyboard,
							{x = x, y = y, width = width, height = height, x2 = x2, y2 = y2, width2 = width2, height2 = height2},
							tokenizeString(columnData, '\n'))
						table.insert(keys, key)

						x = x + width
						maxWidth = math.max(maxWidth, x)
						maxHeight = math.max(maxHeight, y + height)
						-- reset for the next key
						width = 1
						height = 1
						width2, height2, x2, y2 = nil, nil, nil, nil
					end
				end
				y = y + 1
			end

			-- Fix the positions and sizes to fit in our KeyboardContainer
			for _, element in pairs(keys) do
				element.Position = UDim2.new(element.Position.X.Scale / maxWidth, 0, element.Position.Y.Scale / maxHeight, 0)
				element.Size = UDim2.new(element.Size.X.Scale / maxWidth, 0, element.Size.Y.Scale / maxHeight, 0)
				element.Parent = keyboardSizeConstrainer
			end

			keyboardSizeConstrainer.SizeConstraint = Enum.SizeConstraint.RelativeXX
			keyboardSizeConstrainer.Size = UDim2.new(1, 0, -maxHeight / maxWidth, 0)
			keyboardSizeConstrainer.Position = UDim2.new(0, 0, 1, 0)
			keyboardSizeConstrainer.Visible = false

			table.insert(keysets, {keys = keys, container = keyboardSizeConstrainer})
		end
		newKeyboard:SetCurrentKeyset(1)
	end

	textEntryField.MouseButton1Click:connect(function()
		SetTextFieldCursorPosition(calculateTextCursorPosition(panel.lookAtPixel.X, panel.lookAtPixel.Y))
	end)

	closeButton.MouseButton1Click:connect(function()
		newKeyboard:Close(false)
	end)

	voiceDoneButton.MouseButton1Click:connect(function()
		if VoiceToTextFSM:GetCurrentState().Name == "Listening" then
			VoiceToTextFSM:TransitionState(CreateProcessingVoiceState())
		end
	end)

	local function onVoiceProcessingStateChanged(newState)
		if newState.Name == "Listening" then
			voiceProcessingStatus.Text = "Listening..."
		elseif newState.Name == "Processing" then
			voiceProcessingStatus.Text = "Processing..."
		elseif newState.Name == "Waiting" then
			voiceProcessingStatus.Text = "Done"
		end

		-- Get the result and put it into the textfield
		if newState.Name == "Processing" then
			coroutine.wrap(function()
				voiceVisualizer:StopAnimation()
				local result = newState:GetResultAsync()
				if result and result["Status"] == VOICE_STATUS_CODE_ENUM.ASR_STATUS_OK  then
					setBufferText(result["Response"])
				else
					voiceProcessingStatus.Text = "An error occured, please try again."
					wait(2)
				end
				VoiceToTextFSM:TransitionState(CreateWaitingVoiceState())
			end)()
		elseif newState.Name == "Listening" then
			voiceVisualizer:StartAnimation()
		elseif newState.Name == "Waiting" then
			newKeyboard:SetVoiceMode(false)
		end
	end
	VoiceToTextFSM.StateTransitionedEvent:connect(onVoiceProcessingStateChanged)
	onVoiceProcessingStateChanged(VoiceToTextFSM:GetCurrentState())


	return newKeyboard
end


local Keyboard = nil;
local function GetKeyboard()
	if Keyboard == nil then
		Keyboard = ConstructKeyboardUI({MINIMAL_KEYBOARD_LAYOUT, MINIMAL_KEYBOARD_LAYOUT_SYMBOLS})
	end
	return Keyboard
end



local VirtualKeyboardClass = {}

function VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox)
	local keyboardOptions = {}

	keyboardOptions.TextBox = textbox

	return keyboardOptions
end

local VirtualKeyboardPlatform = false
do
	-- iOS, Android and Xbox already have platform specific keyboards
	local platform = UserInputService:GetPlatform()
	VirtualKeyboardPlatform = platform == Enum.Platform.Windows or
	                          platform == Enum.Platform.OSX or
	                          platform == Enum.Platform.IOS or 
	                          platform == Enum.Platform.Android
end


function VirtualKeyboardClass:ShowVirtualKeyboard(virtualKeyboardOptions)
	if VirtualKeyboardPlatform and UserInputService.VREnabled then
		GetKeyboard():Open(virtualKeyboardOptions)
	end
end

function VirtualKeyboardClass:CloseVirtualKeyboard()
	if VirtualKeyboardPlatform and UserInputService.VREnabled then
		local currentKeyboard = GetKeyboard()
		currentKeyboard:Close(false)
	end
end

VirtualKeyboardClass.OpenedEvent = GetKeyboard().OpenedEvent
VirtualKeyboardClass.ClosedEvent = GetKeyboard().ClosedEvent


if VirtualKeyboardPlatform then
	UserInputService.TextBoxFocused:connect(function(textbox)
		VirtualKeyboardClass:ShowVirtualKeyboard(VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox))
	end)
	-- Don't have to hook up to TextBoxFocusReleased because we are already listening to that in keyboard
end


return VirtualKeyboardClass
