local defaultCodes = require(script.DefaultCodes)

local dataStore = nil
local DataStoreService = game:GetService("DataStoreService")
dataStoreKey = script.Parent.dataStoreKey.Value
if dataStoreKey ~= "" then
	dataStore = DataStoreService:GetDataStore('securityCodes-' .. script.Parent.dataStoreKey.Value or "XP-99")
else
	-- Make virtual datastore for offline use
	dataStore = {
		_data = {},
		GetAsync = function(self, key)
			return self._data[key]
		end,
		SetAsync = function(self, key, value)
			self._data[key] = value
		end
	}
end

function setup(parentScript)
	-- dont touch
	local version = "v1.2.4"

	-- codes object, will be swapped for data store later
	codes = dataStore:GetAsync("codes")
	if not codes then
		codes = defaultCodes
		dataStore:SetAsync("codes", codes)
	end

	-- Make a settings object mapped to the values in parentScript.Parent.Settings
	settings = {}
	for i, e in pairs(parentScript.Parent.Settings:GetChildren()) do
		settings[e.Name] = e.Value
	end

	power = false
	chime = true
	armState = 0
	delay = 0
	instant = false
	faults = {}
	fault = false
	alarms = {}
	fires = {}
	zones = {}
	keypads = {}
	countdown = nil

--[[
armState
0 is disarmed
1 is armed stay
2 is armed away
3 is alarm
4 is fire alarm
5 is armed instant (stay no entry delay)
6 is armed maximum (away no entry delay)

delay
0 is inactive
1 is exit delay
	When active, use slow beep, show ARMED ***AWAY***\nYou may exit now or ARMED ***STAY***\nYou may exit now depending on state. During last 10 seconds of delay, switch to fast beep, at each second check if the system has been armed, if it has drop out of the loop
2 is entry delay. When active, use slow beep, show "DISARM SYSTEM\nOr alarm will occur", during last 10 seconds of delay, switch to fast beep. At each second check if the system has been disarmed, if it has drop out of the loop
]]
	ux = require(parentScript.Parent.ScriptModules:WaitForChild("UX"))

	-- Get keypads
	for i, e in pairs(parentScript.Parent.Keypads:GetChildren()) do
		if e:IsA("Model") then table.insert(keypads, e) end
	end

	-- Get zone
	for i, e in ipairs(parentScript.Parent.Zones:GetChildren()) do
		-- add a task that constantly checks e.tripped.Value, and set zones[i] to the value of e.tripped.Value
		zones[e.Name] = e
	end

	-- Funcs
	function rightAlignString(str)
		local strLen = string.len(str)
		local padding = 16 - strLen
		local paddingStr = ""
		for i = 1, padding do paddingStr = paddingStr .. " " end
		return paddingStr .. str
	end

	function playAud(sound) if not sound.Playing then sound:Play() end end

	function stopAud(sound) if sound.Playing then sound:Stop() end end
	local curTone = 0
	local lastTone = 0 -- only really used to go back from button pulse to previous tone
	local beeping = false

	function changeTone(tone) curTone = tone end

	function beep(keypad)

		if not power then return end
		if not beeping then
			task.spawn(function()
				beeping = true
				lastTone = curTone
				playAud(keypad.Display.hi)
				wait(0.08)
				stopAud(keypad.Display.hi)
				wait(0.08)
				beeping = false
			end)
		end
	end

	function ding(count)
		-- Play a ding sound count times, each lasting .3 seconds with a .1 second pause between each
		task.spawn(function()
			for i = 1, count do
				if not power then return end
				for i, keypad in ipairs(keypads) do
					playAud(keypad.Display.ding)
				end
				wait(0.2)
				-- if its not the last ding, stop the sound
				if i ~= count then
					for i, keypad in ipairs(keypads) do
						stopAud(keypad.Display.ding)
					end
				end
			end
		end)
	end

--[[
0 no tone
1 constant
2 pulse (for number input)
3 slow pulse (entry/exit delay main)
4 fast pulse (end of exit/entry delay and trouble)
]]
	function switchKeypadAudio(enabled)
		for i,e in ipairs(script.KeypadAudio:GetChildren()) do
			if e.Name ~= enabled then
				e.Disabled = true
			else
				e.Enabled = true
			end
		end
	end 
	function audioLoop()
		while true do
			wait()
			if not power then curTone = 0 end
			if curTone == 0 then -- Stop tone
				switchKeypadAudio("off")
			elseif curTone == 1 then -- Constant tone
				switchKeypadAudio("constant")
			elseif curTone == 2 then -- Slow pulse tone
				switchKeypadAudio("slowpulse")
			elseif curTone == 3 then -- Fast pulse tone
				switchKeypadAudio("fastpulse")
			elseif curTone == 4 then -- HiLo tone alarm
				switchKeypadAudio("hilo")
			elseif curTone == 5 then -- Fire alarm tone
				switchKeypadAudio("fire")
			end
		end
	end

	function alignText(leftText, rightText, totalWidth)
		local leftLength = #leftText
		local rightLength = #rightText
		local spaceBetween = totalWidth - (leftLength + rightLength)

		-- If the total length of both texts exceeds the totalWidth, return concatenated textif spaceBetween < 0thenreturn leftText .. rightText
		return leftText .. string.rep(" ", spaceBetween) .. rightText
	end

	function displayLoop()
		while true do
			wait()
			if not power then
				for i, keypad in ipairs(keypads) do
					ux.setDisplays(keypads, "", "")
				end
			else
				if armState == 0 then -- We are disarmed, check if there are any faults
					if #faults > 0 then
						-- for however many faults there are, display them one at a time based on faultPage (gets updated every 2 seconds, or when * is pressed)
						--ux.setDisplays(keypads, "FAULT " .. table.concat(faults, ", ", faultPage, faultPage + 1))
						ux.setLEDs(keypads, "ready", false)
						fault = true
						ux.setDisplays(keypads, "FAULT " .. faults[1])
					else
						ux.setLEDs(keypads, "ready", true)
						ux.setLEDs(keypads, "armed", false)
						if not chime then
							ux.setDisplays(keypads, "****DISARMED****", "  READY TO ARM  ")
						else
							ux.setDisplays(keypads, " DISARMED CHIME ", "  READY TO ARM  ")
						end
					end
				elseif armState == 1 then -- We are armed stay
					ux.setLEDs(keypads, "armed", true)
					ux.setLEDs(keypads, "ready", false)
					if delay == 1 then
						ux.setDisplays(keypads, "ARMED ***STAY***", "You may exit now")
					elseif delay == 2 then
						ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.")
					else
						ux.setDisplays(keypads, "ARMED ***STAY***")
					end
				elseif armState == 2 then -- We are armed away
					ux.setLEDs(keypads, "armed", true)
					ux.setLEDs(keypads, "ready", false)
					if delay == 1 then
						if settings.showCountdown then
							ln2 = alignText("May Exit Now", tostring(settings.exitDelay - countdown), 16)
						else
							ln2 = "You may exit now" 
						end
						ux.setDisplays(keypads, "ARMED ***AWAY***", ln2)
					elseif delay == 2 then
						ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.")
					else
						ux.setDisplays(keypads, "ARMED ***AWAY***", "***ALL SECURE***")
					end
				elseif armState == 3 then -- We are in alarm
					ux.setLEDs(keypads, "ready", false)
					if curTone ~= 4 then changeTone(4) end
					-- show the zone number of the alarmed zone
					ux.setDisplays(keypads, "ALARM " .. tostring(alarms[1] or ""))
				elseif armState == 4 then -- We are in fire alarm
					ux.setLEDs(keypads, "ready", false)
					-- show the zone number of the alarmed zone
					ux.setDisplays(keypads, "FIRE " .. tostring(fires[1] or ""))
				elseif armState == 5 then -- Armed instant
					ux.setLEDs(keypads, "armed", true)
					ux.setLEDs(keypads, "ready", false)
					if delay == 1 then
						ux.setDisplays(keypads, "ARMED *INSTANT*", "You may exit now")
					else
						ux.setDisplays(keypads, "ARMED *INSTANT*")
					end
				elseif armState == 6 then -- Armed max
					ux.setLEDs(keypads, "armed", true)
					ux.setLEDs(keypads, "ready", false)
					if delay == 1 then
						if settings.showCountdown then
							ln2 = alignText("May Exit Now", tostring(settings.exitDelay - countdown), 16)
						else
							ln2 = "You may exit now" 
						end
						ux.setDisplays(keypads, "ARMED *MAXIMUM*", ln2)
					else
						ux.setDisplays(keypads, "ARMED *MAXIMUM*", "***ALL SECURE***")
					end
				end
			end
		end

	end

	local input = {}
	local keyClear = nil
	local authenticated = false
	local inputCount = 0
	local inputMode = 0
	local authedUser = nil

	lockout = false
	authAttempts = 0
	function handleInput(key)
		if not power then return end
		if lockout then
			task.spawn(function()
				curTone = 1
				wait(2)
				curTone = 0
			end)
			return
		end -- eventually do long beep 2 seconds here
		if key == "*" then
			deauth()
			return
		end
		keyClear = math.random(1, 10000)
		task.spawn(function() inputTimeout(keyClear) end)
		if not authenticated then
			-- Should work like a honeywell vista, for every number entered 4 or more in buffer, check if the last 4 numbers are a valid code. For every 4 numbers entered, if its not a valid code add one to the attempts
			table.insert(input, key)
			if #input >= 4 then
				local code = table.concat(input, "", #input - 3, #input)
				if table.find(codes, code) then
					authenticated = true
					inputMode = 1
					authAttempts = 0
					keyClear = nil
					input = {}
					authedUser = code
				else
					authAttempts = authAttempts + 1
					if authAttempts >= 3 then
						task.spawn(lockoutFunc)
					end
					keyClear = nil
					input = {}
				end
			end
		elseif authenticated and inputMode == 1 then
			wait(math.random(.5, 1.5))
			if key == "1" then
				disarm()
				deauth()
			elseif key == "2" then
				if armState ~= 0 then return end
				armAway()
				deauth()
			elseif key == "3" then
				if armState ~= 0 then return end
				armStay()
				deauth()
			elseif key == "4" then -- Arm Max
				if armState ~= 0 then return end
				armMax()
				deauth()
			elseif key == "5" then -- Test (TODO)

			elseif key == "6" then -- Bypass (TODO)

			elseif key == "7" then -- Instant
				if armState ~= 0 then return end
				armInstant()
				deauth()
			elseif key == "8" then
				-- If the code used was code 1, and the system is disarmed, Set inputMode to 2 (code change)
				-- See what index the code is at, if its index 1, its the right code, set mode to 2. do NOT hardcode the code here
				print(codes[1] .. " : " .. authedUser)
				if codes[1] == authedUser then
					inputMode = 2
				else
					ding(1)
					deauth()
				end
			elseif key == "9" then
				toggleChime()
				deauth()
			end
		elseif authenticated and inputMode == 2 then
			-- Keep adding inputs together until we have 2+4+4 in the buffer, if both codes match, and arent being used, set the new code to the code at index of the first 2 digits
			-- input data is: 2 digits: user id/index, 4 digits: new code, 4 digits: new code verify
			table.insert(input, key)
			-- check if digit 3 is a hash and 4 a 0, if so delete the user
			if #input >= 4 and input[3] == "#" and input[4] == "0" then
				local uid = table.concat(input, "", 1, 2)
				if tonumber(uid) == 1 then
					ding(4)
					return
				else
					codes[tonumber(uid)] = nil
					dataStore:SetAsync("codes", codes)
					ding(1)
				end
				deauth()
			else
				if #input >= 10 then
					local uid = table.concat(input, "", 1, 2)
					local newCode = table.concat(input, "", 3, 6)
					local newCodeVerify = table.concat(input, "", 7, 10)
					if newCode == newCodeVerify then
						if not table.find(codes, newCode) then
							codes[tonumber(uid)] = newCode
							dataStore:SetAsync("codes", codes)
							ding(1)
						else
							ding(4)
						end
					else
						ding(4)
					end
					deauth()
				end
			end
		end
	end

	function deauth()
		keyClear = nil
		authenticated = false
		input = {}
		inputMode = 0
	end

	function toggleChime()
		if chime then
			ding(2)
		else
			ding(1)
		end
		chime = not chime
		deauth()
	end

	function disarm()
		deauth()
		task.spawn(function()
			-- Reset any smoke zones
			for i,e in pairs(zones) do
				if e:FindFirstChild("Reset") then
					e.Reset.Value = true
					wait(5)
					e.Reset.Value = false
				end
			end
		end)
		wait(.1)
		armState = 0
		changeTone(0)
		alarms = {}
		fires = {}
		faults = {}
		ding(1)
	end

	function armStay()
		if #faults > 0 then
			ding(1)
			return
		end
		armState = 1
		delay = 1
		task.spawn(function()
			ding(3)
			for i = 1, settings.exitDelay do
				if armState ~= 1 then
					delay = 0
					countdown = 0
					return
				end
				countdown = i
				wait(1)
				if i >= settings.exitDelay then delay = 0 countdown = 0 end
			end
		end)
	end

	function armAway()
		if #faults > 0 then
			ding(1)
			return
		end
		armState = 2
		delay = 1
		task.spawn(function()
			changeTone(2)
			for i = 1, settings.exitDelay do
				if armState ~= 2 then
					changeTone(0)
					delay = 0
					countdown = 0
					return
				end
				countdown = i
				wait(1)
				if i >= settings.exitDelay - 10 then changeTone(3) end
				if i >= settings.exitDelay then
					changeTone(0)
					delay = 0
					countdown = 0
				end
			end
		end)
	end

	function armMax()
		if #faults > 0 then
			ding(1)
			return
		end
		armState = 6
		delay = 1
		task.spawn(function()
			changeTone(2)
			for i = 1, settings.exitDelay do
				if armState ~= 6 then
					changeTone(0)
					delay = 0
					countdown = 0
					return
				end
				countdown = i
				wait(1)
				if i >= settings.exitDelay - 10 then changeTone(3) end
				if i >= settings.exitDelay then
					changeTone(0)
					delay = 0
					countdown = 0
				end
			end
		end)
	end

	function armInstant()
		if #faults > 0 then
			ding(1)
			return
		end
		armState = 5
		delay = 1
		task.spawn(function()
			ding(3)
			for i = 1, settings.exitDelay do
				if armState ~= 5 then
					delay = 0
					countdown = 0
					return
				end
				countdown = i
				wait(1)
				if i >= settings.exitDelay then delay = 0 countdown = 0 end
			end
		end)
	end

	function inputTimeout(myNum)
		wait(10)
		if keyClear ~= myNum then return end
		authenticated = false
		input = {}
	end

	function lockoutFunc()
		lockout = true
		changeTone(1)
		wait(2)
		changeTone(0)
		wait(60)
		lockout = false
	end
	-- Button Click Handler (each keypad has Buttons folder with parts that have a clickdetector nested within, add an event handler for each that adds the button to the input buffer)
	function buttonClickHandler(button)
		button.MouseClick:Connect(function()
			if not power then return end
			task.spawn(function() beep(button.Parent.Parent.Parent) end)
			wait(0.05)
			handleInput(button.Parent.Name)
		end)
	end

	for i, keypad in ipairs(keypads) do
		for j, button in ipairs(keypad.Buttons:GetChildren()) do
			buttonClickHandler(button.ClickDetector)
		end
	end

	-- Arming Functions

	function boot()
		task.spawn(audioLoop)
		power = true
		wait()
		ux.displayPower(keypads, true)
		ux.setLEDs(keypads, "*", true)
		ux.setDisplays(keypads, "################", "################")
		changeTone(1)
		wait(1)
		changeTone(0)
		ux.setLEDs(keypads, "*", false)
		ux.setLEDs(keypads, "power", true)
		ux.setDisplays(keypads, "Busy - Standby", rightAlignString(version))
		wait(5)
		task.spawn(displayLoop)
		task.spawn(handleZones)
		task.spawn(sirenLoop)
		task.spawn(stateLoop)

	end

	function stateLoop()
		while true do
			if #alarms > 0 then
				script.Parent.Reporting.alarmZone.Value = alarms[1]
			else
				script.Parent.Reporting.alarmZone.Value = -1
			end
			if #fires > 0 then
				script.Parent.Reporting.fireZone.Value = fires[1]
			else
				script.Parent.Reporting.fireZone.Value = -1
			end
			script.Parent.Reporting.armState.Value = armState
			script.Parent.Reporting.delay.Value = delay
			script.Parent.Reporting.curTone.Value = curTone
			wait(0)
		end
	end

	function handleZones()
		while true do
			wait()
			-- Always: If a zone is tripped, add it to the faults table if its not already there
			-- If the zone is not tripped remove it from the faults table if its there
			-- If the zone is tripped and the system is armed, check if its an instant zone, if it is, set the alarm state to 3, otherwise trigger entry delay
			-- If the system is in alarm, check if the zone is tripped, if it is, add it to the alarms table if its not already there
			for i, zone in pairs(zones) do
				-- if the zone is tripped add it to the faults table if its not already there
				if zone.tripped.Value then
					-- if the zone is already in faults, continue
					if not table.find(faults, zone.Name) then
						if not table.find(faults, zone.Name) then
							table.insert(faults, zone.Name)
						end
						if zone.isFire.Value then
							armState = 4
							changeTone(5)
							table.insert(fires, zone.Name)
						end

						if zone.alwaysOn.Value then -- This zone is a 24/7 audible zone, go straight to alarm
							armState = 3
							changeTone(4)
							table.insert(alarms, zone.Name)
						end

						if armState == 0 then
							if zone:FindFirstChild("chime") then
								if chime and zone.chime.Value then
									ding(3)
								end
							end
						elseif armState == 1 and delay == 0 then
							-- if the zone is interior, ignore
							if zone.interior.Value then
								-- ignore
							else
								-- Check if the zone is instant or not
								-- If it is, set the alarm state to 3
								-- If it is not, trigger entry delay
								if zone.instant.Value then
									-- Add to alarms
									table.insert(alarms, zone.Name)
									armState = 3
									changeTone(4)
								else
									delay = 2
									task.spawn(function()
										table.insert(alarms, zone.Name)
										changeTone(2)
										for i = 1, settings.entryDelay do
											if armState ~= 1 then
												changeTone(0)
												delay = 0
												return
											end
											wait(1)
											if i >= settings.entryDelay - 10 then
												changeTone(3)
											end
											if i >= settings.entryDelay then
												-- at this point the system has not been disarmed, go to alarm
												changeTone(4)
												delay = 0
												armState = 3
											end
										end
									end)
								end
							end
						elseif armState == 2 and delay == 0 then
							-- Check if the zone is instant or not
							-- If it is, set the alarm state to 3
							-- If it is not, trigger entry delay
							if zone.instant.Value then
								-- Add to alarms
								table.insert(alarms, zone.Name)
								armState = 3
								changeTone(4)
							else
								table.insert(alarms, zone.Name)
								delay = 2
								task.spawn(function()
									changeTone(2)
									for i = 1, settings.entryDelay do
										if armState ~= 2 then
											changeTone(0)
											delay = 0
											return
										end
										wait(1)
										if i >= settings.entryDelay - 10 then
											changeTone(3)
										end
										if i >= settings.entryDelay then
											-- at this point the system has not been disarmed, go to alarm

											changeTone(4)
											delay = 0
											armState = 3
										end
									end
								end)
							end
						elseif armState == 3 then
							-- if the zone is already in alarms, continue
							if not table.find(alarms, zone.Name) then
								table.insert(alarms, zone.Name)
								changeTone(4)
							end
						elseif armState == 5 and delay == 0 then -- Armed instant, 
							if not zone.interior.Value then
								table.insert(alarms, zone.Name)
								armState = 3
								changeTone(4)
							end
						elseif armState == 6 and delay == 0 then
							table.insert(alarms, zone.Name)
							armState = 3
							changeTone(4)
						end
					end
				else
					if table.find(faults, zone.Name) then
						table.remove(faults, table.find(faults, zone.Name))
					end
				end
			end
		end
	end

	function sirenLoop() -- For external sirens
		while true do
			wait()
			if armState == 3 then
				-- Turn on siren
				for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
					if not e.Speaker.siren.playing then
						e.Speaker.siren:Play()
					end
				end
			elseif armState == 4 then -- Fire alarm
				-- Handle dedicated fire sirens
				for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do
					if not e.Speaker.siren.playing then
						e.Speaker.siren:Play()
					end
				end
				-- Make this do temporal 3
				for i = 1, 3 do
					for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
						if not e.Speaker.siren.playing then
							e.Speaker.siren:Play()
						end
					end
					wait(.5)
					for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
						if e.Speaker.siren.playing then
							e.Speaker.siren:Stop()
						end
					end
					wait(.5)
				end
				wait(1)
			else
				-- Turn off siren
				for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do
					if e.Speaker.siren.playing then
						e.Speaker.siren:Stop()
					end
				end
				for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do
					if e.Speaker.siren.playing then
						e.Speaker.siren:Stop()
					end
				end
			end
		end

	end
	boot()
end

setup(script)