--[[
	Pkce

	Generates a (codeVerifier, codeChallenge) pair for the OAuth2
	Authorization Code + PKCE flow (RFC 7636, S256 method).

	Roblox doesn't ship a crypto module, so we:
	  • derive 32 random bytes from two HttpService GUIDs (each ≈122 bits),
	  • base64url-encode them (no padding) to form the verifier,
	  • SHA-256 the verifier and base64url-encode the digest for the challenge.

	The dekkonot/sha256 package is already vendored (used by Telemetry).
	Its second return value is the digest as 8 unsigned 32-bit integers,
	which we pack into raw bytes via string.pack to avoid re-parsing the
	hex string back into bytes.
]]

local HttpService = game:GetService("HttpService")

local Plugin = script:FindFirstAncestor("StudioActivity")
local Packages = Plugin.Packages
local sha256 = require(Packages.sha256)

local Pkce = {}

export type Keys = {
	codeVerifier: string,
	codeChallenge: string,
}

-- Standard base64 alphabet, but with `+`/`/` swapped for `-`/`_` per
-- RFC 4648 §5. Padding (`=`) is omitted, also per RFC 7636 §4.2.
local BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

local function charAt(sextet: number): string
	return string.sub(BASE64URL_ALPHABET, sextet + 1, sextet + 1)
end

local function base64UrlEncode(bytes: string): string
	local out = table.create(math.ceil(#bytes / 3) * 4)
	local len = #bytes

	-- Process complete 3-byte groups → 4 base64 chars.
	local i = 1
	while i + 2 <= len do
		local b1, b2, b3 = string.byte(bytes, i, i + 2)
		local n = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2, 8), b3)
		table.insert(out, charAt(bit32.rshift(n, 18)))
		table.insert(out, charAt(bit32.band(bit32.rshift(n, 12), 0x3f)))
		table.insert(out, charAt(bit32.band(bit32.rshift(n, 6), 0x3f)))
		table.insert(out, charAt(bit32.band(n, 0x3f)))
		i += 3
	end

	-- Tail: 1 or 2 remaining bytes → 2 or 3 base64 chars (no padding).
	local remaining = len - i + 1
	if remaining == 1 then
		local b1 = string.byte(bytes, i)
		local n = bit32.lshift(b1, 16)
		table.insert(out, charAt(bit32.rshift(n, 18)))
		table.insert(out, charAt(bit32.band(bit32.rshift(n, 12), 0x3f)))
	elseif remaining == 2 then
		local b1, b2 = string.byte(bytes, i, i + 1)
		local n = bit32.bor(bit32.lshift(b1, 16), bit32.lshift(b2, 8))
		table.insert(out, charAt(bit32.rshift(n, 18)))
		table.insert(out, charAt(bit32.band(bit32.rshift(n, 12), 0x3f)))
		table.insert(out, charAt(bit32.band(bit32.rshift(n, 6), 0x3f)))
	end

	return table.concat(out)
end

--[[
	Compute the PKCE S256 challenge for an existing verifier:
	  challenge = base64url(SHA256(verifier)),  no padding
]]
local function deriveChallenge(codeVerifier: string): string
	local _hex, words = sha256(codeVerifier)
	-- Pack the 8 u32 digest words into 32 raw bytes (big-endian).
	local digestBytes =
		string.pack(">I4I4I4I4I4I4I4I4", words[1], words[2], words[3], words[4], words[5], words[6], words[7], words[8])
	return base64UrlEncode(digestBytes)
end

--[[
	Build a new verifier + challenge pair.

	Verifier construction (matches RFC 7636 §4.1's recommendation of
	base64url(32 random octets)):
	  1. Generate two v4 GUIDs and concatenate the hex (64 hex chars).
	  2. Decode to 32 raw bytes.
	  3. base64url-encode → 43 chars, no padding.
]]
function Pkce.generate(): Keys
	local guidA = HttpService:GenerateGUID(false)
	local guidB = HttpService:GenerateGUID(false)
	local hex = string.gsub(guidA .. guidB, "-", "")

	local bytes = table.create(32)
	for i = 1, 32 do
		bytes[i] = string.char(tonumber(string.sub(hex, i * 2 - 1, i * 2), 16) :: number)
	end
	local randomBytes = table.concat(bytes)

	local codeVerifier = base64UrlEncode(randomBytes)
	local codeChallenge = deriveChallenge(codeVerifier)

	return {
		codeVerifier = codeVerifier,
		codeChallenge = codeChallenge,
	}
end

-- Exposed for unit tests (RFC 7636 Appendix B vectors).
Pkce._deriveChallenge = deriveChallenge
Pkce._base64UrlEncode = base64UrlEncode

return Pkce


local Plugin = script:FindFirstAncestor("StudioActivity")

local Authentication = Plugin.Source.Authentication
local Packages = Plugin.Packages
local JestGlobals = require(Packages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

local Pkce = require(Authentication.Pkce)

describe("Pkce", function()
	describe("base64UrlEncode", function()
		it("encodes empty input as empty string", function()
			expect(Pkce._base64UrlEncode("")).toBe("")
		end)

		it("encodes single byte without padding", function()
			-- 0x00 → 6+2 bits → "AA"
			expect(Pkce._base64UrlEncode("\0")).toBe("AA")
		end)

		it("encodes two bytes without padding", function()
			-- 0xff 0xff → "//+8" in std b64 → "__8" in url-safe (no padding)
			expect(Pkce._base64UrlEncode("\xff\xff")).toBe("__8")
		end)

		it("encodes three bytes as four chars", function()
			-- "ABC" (0x41 0x42 0x43) → standard b64 "QUJD"
			expect(Pkce._base64UrlEncode("ABC")).toBe("QUJD")
		end)

		it("uses url-safe alphabet (no + or /)", function()
			-- 0xfb 0xff → "+/" in std b64; should become "-_" in url-safe
			-- Note: 0xfb 0xff produces "+/8" in std → "-_8" url-safe (no pad)
			expect(Pkce._base64UrlEncode("\xfb\xff")).toBe("-_8")
		end)

		it("encodes 32 bytes to 43 chars (no padding)", function()
			local bytes = string.rep("\0", 32)
			expect(#Pkce._base64UrlEncode(bytes)).toBe(43)
		end)
	end)

	describe("deriveChallenge (RFC 7636 Appendix B)", function()
		it("matches the canonical S256 test vector", function()
			-- Per RFC 7636 Appendix B:
			--   verifier  = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
			--   challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
			local verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
			local expected = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
			expect(Pkce._deriveChallenge(verifier)).toBe(expected)
		end)
	end)

	describe("generate", function()
		it("returns a verifier matching [A-Za-z0-9_-]{43}", function()
			local keys = Pkce.generate()
			expect(#keys.codeVerifier).toBe(43)
			expect(string.match(keys.codeVerifier, "^[A-Za-z0-9_-]+$")).toBe(keys.codeVerifier)
		end)

		it("returns a 43-char challenge", function()
			local keys = Pkce.generate()
			expect(#keys.codeChallenge).toBe(43)
			expect(string.match(keys.codeChallenge, "^[A-Za-z0-9_-]+$")).toBe(keys.codeChallenge)
		end)

		it("challenge is the SHA-256 of the verifier (round-trip)", function()
			local keys = Pkce.generate()
			expect(Pkce._deriveChallenge(keys.codeVerifier)).toBe(keys.codeChallenge)
		end)

		it("returns distinct values across calls", function()
			local a = Pkce.generate()
			local b = Pkce.generate()
			expect(a.codeVerifier).never.toBe(b.codeVerifier)
		end)
	end)
end)


export type BuildVars = {
	build: {
		version: string,
		channel: string,
		hash: string,
		isDev: boolean,
	},

	api: {
		secure: boolean,
		host: string,
	},

	discord: {
		clientId: string,
	},
}

return nil


local Types = require(script.Types)
export type BuildVars = Types.BuildVars

local CompiledBuildVars: BuildVars = require(script.BuildVars)
return table.freeze(CompiledBuildVars)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local PluginStore = require(Plugin.Source.Plugin.PluginStore)
local useThemeName = require(script.Parent.useThemeName)

local FoundationProvider = Foundation.FoundationProvider

local e = React.createElement
local useEffect = React.useEffect

type Props = {
	plugin: Plugin,
	overlayGui: GuiBase2d?,

	_TESTING_themeOverride: Foundation.Theme?,

	children: React.ReactNode,
}

local function ContextProviders(props: Props)
	local themeName: useThemeName.ThemeName = useThemeName()

	useEffect(function()
		PluginStore.get().setPlugin(props.plugin)
	end, { props.plugin })

	return e(ReactUtils.ContextStack, {
		providers = {
			e(FoundationProvider, {
				theme = props._TESTING_themeOverride or themeName,
				overlayGui = props.overlayGui,
			}),
		},
	}, props.children)
end

return ContextProviders


--[[
	Trailing-edge debounce: when called repeatedly within a window, only the
	last invocation fires after the window expires.

	Usage:

		local debounced = Debounce.trailing(2, function(value)
			print("final value:", value)
		end)

		debounced.call("a")  -- schedules fire in 2s
		debounced.call("b")  -- resets timer, "a" is dropped
		-- 2s later: prints "final value: b"

		debounced.cancel()   -- cancels any pending call
		debounced.flush()    -- fires immediately if pending, then resets
]]

export type DebouncedFn = {
	call: (...any) -> (),
	cancel: () -> (),
	flush: () -> (),
}

local Debounce = {}

function Debounce.trailing(delaySeconds: number, fn: (...any) -> ()): DebouncedFn
	local pendingThread: thread? = nil
	local pendingArgs: { any }? = nil

	local function cancel()
		if pendingThread then
			pcall(task.cancel, pendingThread)
			pendingThread = nil
		end
		pendingArgs = nil
	end

	local function flush()
		if pendingArgs then
			local args = pendingArgs
			cancel()
			fn(table.unpack(args))
		end
	end

	local function call(...)
		pendingArgs = { ... }

		if pendingThread then
			pcall(task.cancel, pendingThread)
		end

		pendingThread = task.delay(delaySeconds, function()
			pendingThread = nil
			local args = pendingArgs
			pendingArgs = nil
			if args then
				fn(table.unpack(args))
			end
		end)
	end

	return table.freeze({
		call = call,
		cancel = cancel,
		flush = flush,
	})
end

return table.freeze(Debounce)


local AssetService = game:GetService("AssetService")
local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local png = require(Packages.png)

local Logger = require(Plugin.Source.Logger)

local DiscordApi = require(Plugin.Source.Api.Discord)
type AvatarImage = DiscordApi.AvatarImage

local View = Foundation.View
local Icon = Foundation.Icon
local IconName = Foundation.Enums.IconName
local IconSize = Foundation.Enums.IconSize

local e = React.createElement
local useEffect = React.useEffect
local useState = React.useState

type Props = {
	avatar: AvatarImage,
	size: number,
	isDimmed: boolean,
	LayoutOrder: number?,
}

local function DiscordAvatar(props: Props)
	local imageContent, setImageContent = useState<<Content?>>(nil)
	local hasError, setHasError = useState(false)

	useEffect(function()
		if props.avatar.contentType ~= "image/png" then
			setHasError(true)
			return
		end

		local editableImage
		local success, err = pcall(function()
			local pixelBuf = buffer.fromstring(props.avatar.data)
			local decoded = png.decode(pixelBuf)

			local size = Vector2.new(decoded.width, decoded.height)
			editableImage = AssetService:CreateEditableImage({
				Size = size,
			})

			editableImage:WritePixelsBuffer(Vector2.zero, size, decoded.pixels)

			-- TODO: Migrate to the static content API once available. Should save
			-- a bit of memory.
			setImageContent(Content.fromObject(editableImage))
		end)

		if not success then
			setHasError(true)
			Logger:Warn("Error while rendering Discord avatar", {
				error = err,
			})
		else
			setHasError(false)
		end

		return function()
			if editableImage then
				editableImage:Destroy()
			end
			setImageContent(nil)
		end
	end, { props.avatar })

	return e(View, {
		tag = "bg-surface-100 radius-circle row align-y-center align-x-center",
		Size = UDim2.fromOffset(props.size, props.size),
		LayoutOrder = props.LayoutOrder,
	}, {
		ErrorIcon = hasError and e(Icon, {
			name = IconName.SquareFaceEmoji,
			size = IconSize.Medium,
		}),

		Image = (not hasError) and e("ImageLabel", {
			Size = UDim2.fromScale(1, 1),
			BackgroundTransparency = 1,
			ImageTransparency = if props.isDimmed then 0.6 else 0,
			ImageContent = imageContent,
		}, {
			UICorner = e("UICorner", {
				CornerRadius = UDim.new(1, 0),
			}),
		}),
	})
end

return React.memo(DiscordAvatar)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local View = Foundation.View
local Icon = Foundation.Icon
local IconName = Foundation.Enums.IconName
local IconSize = Foundation.Enums.IconSize
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	size: "small" | "big",
	LayoutOrder: number?,
}

local function PluginWordmark(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	local isBig = props.size == "big"
	local logoSize: Foundation.IconSize = if isBig then IconSize.Large else IconSize.Medium
	local dividerSize: Foundation.IconSize = if isBig then IconSize.Medium else IconSize.Small

	return e(View, {
		tag = {
			["auto-xy row align-y-center"] = true,
			["gap-xsmall"] = isBig,
			["gap-xxsmall"] = not isBig,
		},
		LayoutOrder = props.LayoutOrder,
	}, {
		Discord = e(Icon, {
			name = IconName.Discord,
			size = logoSize,
			style = tokens.Color.Content.Emphasis,
			LayoutOrder = nextOrder(),
		}),

		ArrowIcon = e(Icon, {
			name = IconName.DualArrowsHorizontal,
			size = dividerSize,
			LayoutOrder = nextOrder(),
		}),

		Studio = e(Icon, {
			name = IconName.Studio,
			size = logoSize,
			style = tokens.Color.Content.Emphasis,
			LayoutOrder = nextOrder(),
		}),
	})
end

return React.memo(PluginWordmark)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Common = Plugin.Source.Common
local Packages = Plugin.Packages
local JestGlobals = require(Packages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it
local jest = JestGlobals.jest
local beforeEach = JestGlobals.beforeEach
local afterEach = JestGlobals.afterEach

local Debounce = require(Common.Debounce)

describe("Debounce.trailing", function()
	beforeEach(function()
		jest.useFakeTimers()
	end)

	afterEach(function()
		jest.useRealTimers()
	end)

	it("should fire after the delay expires", function()
		local calls = {}
		local debounced = Debounce.trailing(2, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		expect(#calls).toBe(0)

		jest.advanceTimersByTime(2000)
		expect(#calls).toBe(1)
		expect(calls[1]).toBe("a")
	end)

	it("should collapse rapid calls to only the last", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		debounced.call("b")
		debounced.call("c")

		jest.advanceTimersByTime(1000)
		expect(#calls).toBe(1)
		expect(calls[1]).toBe("c")
	end)

	it("should reset the timer on each call", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		jest.advanceTimersByTime(500)
		expect(#calls).toBe(0)

		debounced.call("b")
		jest.advanceTimersByTime(500)
		expect(#calls).toBe(0)

		jest.advanceTimersByTime(500)
		expect(#calls).toBe(1)
		expect(calls[1]).toBe("b")
	end)

	it("should not fire after cancel", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		debounced.cancel()

		jest.advanceTimersByTime(2000)
		expect(#calls).toBe(0)
	end)

	it("should fire immediately on flush", function()
		local calls = {}
		local debounced = Debounce.trailing(10, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		debounced.flush()

		expect(#calls).toBe(1)
		expect(calls[1]).toBe("a")
	end)

	it("should not fire again after flush when timer expires", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.call("a")
		debounced.flush()
		jest.advanceTimersByTime(2000)

		expect(#calls).toBe(1)
	end)

	it("should do nothing when flush is called with no pending call", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.flush()
		expect(#calls).toBe(0)
	end)

	it("should pass multiple arguments", function()
		local captured = {}
		local debounced = Debounce.trailing(1, function(a, b, c)
			captured = { a, b, c }
		end)

		debounced.call(1, "two", true)
		jest.advanceTimersByTime(1000)

		expect(captured[1]).toBe(1)
		expect(captured[2]).toBe("two")
		expect(captured[3]).toBe(true)
	end)

	it("should work with zero-arg functions", function()
		local callCount = 0
		local debounced = Debounce.trailing(1, function()
			callCount += 1
		end)

		debounced.call()
		debounced.call()
		jest.advanceTimersByTime(1000)

		expect(callCount).toBe(1)
	end)

	it("should allow new calls after the timer fires", function()
		local calls = {}
		local debounced = Debounce.trailing(1, function(value)
			table.insert(calls, value)
		end)

		debounced.call("first")
		jest.advanceTimersByTime(1000)

		debounced.call("second")
		jest.advanceTimersByTime(1000)

		expect(#calls).toBe(2)
		expect(calls[1]).toBe("first")
		expect(calls[2]).toBe("second")
	end)
end)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Common = Plugin.Source.Common
local Packages = Plugin.Packages
local JestGlobals = require(Packages.JestGlobals)

local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it

local formatElapsedTime = require(Common.formatElapsedTime)

describe("formatElapsedTime", function()
	describe("seconds only (under 1 minute)", function()
		it("should format 0 seconds as 0:00", function()
			expect(formatElapsedTime(0)).toBe("0:00")
		end)

		it("should zero-pad single digit seconds", function()
			expect(formatElapsedTime(3)).toBe("0:03")
			expect(formatElapsedTime(9)).toBe("0:09")
		end)

		it("should format double digit seconds", function()
			expect(formatElapsedTime(10)).toBe("0:10")
			expect(formatElapsedTime(59)).toBe("0:59")
		end)
	end)

	describe("minutes and seconds (under 1 hour)", function()
		it("should format 1 minute exactly", function()
			expect(formatElapsedTime(60)).toBe("1:00")
		end)

		it("should format minutes with zero-padded seconds", function()
			expect(formatElapsedTime(63)).toBe("1:03")
			expect(formatElapsedTime(125)).toBe("2:05")
		end)

		it("should format larger minute values", function()
			expect(formatElapsedTime(600)).toBe("10:00")
			expect(formatElapsedTime(3599)).toBe("59:59")
		end)
	end)

	describe("hours, minutes, and seconds (1 hour or more)", function()
		it("should format 1 hour exactly", function()
			expect(formatElapsedTime(3600)).toBe("1:00:00")
		end)

		it("should zero-pad minutes and seconds", function()
			expect(formatElapsedTime(3603)).toBe("1:00:03")
			expect(formatElapsedTime(3663)).toBe("1:01:03")
		end)

		it("should format multiple hours", function()
			expect(formatElapsedTime(7200)).toBe("2:00:00")
			expect(formatElapsedTime(36000)).toBe("10:00:00")
		end)

		it("should handle complex time values", function()
			-- 2 hours, 30 minutes, 45 seconds = 9045 seconds
			expect(formatElapsedTime(9045)).toBe("2:30:45")
			-- 12 hours, 5 minutes, 3 seconds = 43503 seconds
			expect(formatElapsedTime(43503)).toBe("12:05:03")
		end)
	end)
end)


--[[
	Formats an elapsed time in seconds into a human-readable string.

	The format varies based on duration:
	- Under 1 hour: "MM:SS" (e.g., "5:03", "45:59")
	- 1 hour or more: "H:MM:SS" (e.g., "1:05:03", "12:45:59")

	Seconds and minutes (when hours are present) are always zero-padded to 2 digits.
]]

local function formatElapsedTime(elapsedSeconds: number): string
	local hours = math.floor(elapsedSeconds / 3600)
	local minutes = math.floor((elapsedSeconds % 3600) / 60)
	local seconds = elapsedSeconds % 60

	local paddedSeconds = string.format("%02d", seconds)

	if hours > 0 then
		local paddedMinutes = string.format("%02d", minutes)
		return `{hours}:{paddedMinutes}:{paddedSeconds}`
	else
		return `{minutes}:{paddedSeconds}`
	end
end

return formatElapsedTime


local GuiService = game:GetService("GuiService")
local UserInputService = game:GetService("UserInputService")

local function getResolutionScale(): number
	-- This definitely won't work currently, but might work in the future
	local success, scale: number = pcall((GuiService :: any).GetResolutionScale, GuiService)

	if not success then
		if GuiService:IsTenFootInterface() then
			scale = 3
		else
			-- This is a horrible hack, but `GuiService::GetResolutionScale` is a
			-- privileged API. KeyCode images contain the resolution scale in the
			-- file name: `ButtonA@2x.png`.
			local ok, scaleHack = pcall(function()
				local buttonContentId = UserInputService:GetImageForKeyCode(Enum.KeyCode.ButtonA)
				local buttonScale = buttonContentId:lower():match("@([%d%.]+)x%.%w+$")

				return tonumber(buttonScale or 1) or 1
			end)

			scale = if ok then scaleHack else 1
		end
	end

	return scale
end

return getResolutionScale


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local ReactCharm = require(Packages.ReactCharm)

local PluginStore = require(Plugin.Source.Plugin.PluginStore)

local function usePlugin(): Plugin
	local pluginStore = ReactCharm.useSignalState(PluginStore.get)
	local plugin = pluginStore.getPlugin()
	if plugin == nil then
		error("Plugin not found")
	end
	return plugin
end

return usePlugin


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)

local useState = React.useState
local useEffect = React.useEffect

local function useRefProperty<T>(ref: { current: any? }, propName: string, fallbackValue: T): T
	local propValue, setPropValue = useState(fallbackValue)

	useEffect(function()
		if not ref.current then
			setPropValue(fallbackValue)
			return
		end

		setPropValue((ref.current :: any)[propName])
		local connection = ref.current:GetPropertyChangedSignal(propName):Connect(function()
			setPropValue((ref.current :: any)[propName])
		end)

		return function()
			connection:Disconnect()
		end
	end, { ref.current, propName, fallbackValue } :: { any })

	return propValue
end

return useRefProperty


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)

local UserSettingsStore = require(Plugin.Source.UserSettings.UserSettingsStore)

local useSignalState = ReactCharm.useSignalState

local useMemo = React.useMemo
local useState = React.useState
local useEffect = React.useEffect

export type ThemeName = "Dark" | "Light"

local MOCK_STUDIO = {
	ThemeChanged = Instance.new("BindableEvent").Event,
	Theme = {
		Name = "Light",
	},
}

local function useThemeName(): ThemeName
	local userSettings = useSignalState(UserSettingsStore.get().getStorage)
	local themeOverride = userSettings.theme

	local studio: Studio = useMemo(function()
		local success, result = pcall(function()
			return (settings() :: any).Studio
		end)

		return if success then result else MOCK_STUDIO
	end, {})

	local studioTheme, setStudioTheme = useState(studio.Theme.Name)

	local theme = useMemo(function(): ThemeName
		if themeOverride ~= "system" then
			return if themeOverride == "light" then "Light" else "Dark"
		end

		if studioTheme == "Dark" or studioTheme == "Light" then
			return studioTheme
		end
		return "Dark"
	end, { studioTheme, themeOverride })

	useEffect(function(): any
		local connection = studio.ThemeChanged:Connect(function()
			setStudioTheme(studio.Theme.Name)
		end)

		return function()
			connection:Disconnect()
		end
	end, { studio })

	return theme :: ThemeName
end

return useThemeName


local AssetService = game:GetService("AssetService")

local isShadowAvailable = pcall(function()
	Instance.new("UIShadow"):Destroy()
end)

local isDataModelContentApiAvailable = pcall(function()
	local editableImage = AssetService:CreateEditableImage({
		Size = Vector2.one,
	})

	AssetService:CreateDataModelContentAsync(Content.fromObject(editableImage))
	editableImage:Destroy()
end)

local Flags = {
	isShadowAvailable = isShadowAvailable,
	isDataModelContentApiAvailable = isDataModelContentApiAvailable,
}

return table.freeze(Flags)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Log = require(Packages.Log)

local function stringToLogLevel(logLevel: string)
	if logLevel == "info" then
		return Log.LogLevel.Information
	elseif logLevel == "warn" or logLevel == "warning" then
		return Log.LogLevel.Warning
	elseif logLevel == "debug" then
		return Log.LogLevel.Debugging
	elseif logLevel == "err" or logLevel == "error" then
		return Log.LogLevel.Error
	end
	return Log.LogLevel.Warning
end

local sinks = {}
if _G.__DEV__ then
	table.insert(sinks, Log.Common.RobloxLogSink())
end

local logger = Log.Logger.new(
	{
		Log.Common.Enrichers.LogLevelEnricher(),
		Log.Common.Enrichers.DynSourceEnricher(true),
	},
	sinks,
	{
		Log.Common.Filters.MinLevelFilter(stringToLogLevel(_G.__LOG_LEVEL__)),
	}
)

return logger


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)
local ReactUtils = require(Packages.ReactUtils)

local AccountStore = require(Plugin.Source.Authentication.AccountStore)
local DiscordAvatar = require(Plugin.Source.Common.DiscordAvatar)
local ModalStore = require(Plugin.Source.Modal.ModalStore)
local NotificationStore = require(Plugin.Source.Notification.NotificationStore)
local PresenceManager = require(Plugin.Source.PresenceManager)
local useAccountStatus = require(script.Parent.useAccountStatus)

local createAccountStore = require(Plugin.Source.Authentication.AccountStore.createAccountStore)
type LocalUser = createAccountStore.LocalUser

local View = Foundation.View
local Text = Foundation.Text
local Button = Foundation.Button
local IconButton = Foundation.IconButton
local Toggle = Foundation.Toggle
local Skeleton = Foundation.Skeleton
local Menu = Foundation.Menu
local ButtonVariant = Foundation.Enums.ButtonVariant
local InputSize = Foundation.Enums.InputSize
local IconName = Foundation.Enums.IconName
local Radius = Foundation.Enums.Radius
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder
local useToggleState = ReactUtils.useToggleState
local useSignalState = ReactCharm.useSignalState

local e = React.createElement
local useCallback = React.useCallback
local useMemo = React.useMemo

type Props = {
	accountId: string,
	localUser: LocalUser,
	LayoutOrder: number?,
}

local function AccountEntry(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()
	local user = props.localUser

	local presenceManager = PresenceManager.get()
	local isPresenceEnabled = useSignalState(presenceManager.getIsActive)
	local disabledIds = useSignalState(presenceManager.getDisabledAccountIds)

	local isActive = not disabledIds[props.accountId]

	local toggleActive = useCallback(function()
		presenceManager.setAccountActive(props.accountId, not isActive)
	end, { presenceManager :: any, props.accountId, isActive })

	local accountStore = AccountStore.get()
	local avatarImage = useSignalState(Charm.computed(function()
		local cachedImages = accountStore.cachedUserImages()
		return cachedImages[props.accountId]
	end))

	local status = useAccountStatus(isActive, isPresenceEnabled, user)
	local details = if user.status == "ready"
		then user.details
		elseif user.status == "error" then user.cachedDetails
		else nil

	local menuState = useToggleState(false)
	local menuItems = useMemo(function(): Foundation.MenuItems
		return {
			{
				id = "remove",
				icon = IconName.TrashCan,
				text = "Disconnect Account",
				onActivated = function()
					menuState.disable()

					-- Get confirmation before disconnecting the account
					local modalStore = ModalStore.get()
					task.spawn(function()
						local result = modalStore.openAsync("confirmation", {
							props = {
								title = "Are you sure?",
								body = "If you disconnect the account, you'll have to re-link it.",
							},
						})

						if result then
							accountStore.removeUser(props.accountId)

							local username = if details then details.username else nil
							NotificationStore.get()
								.push("info", if username then `Disconnected account {username}` else "Disconnected account")
						end
					end)
				end,
			},
		}
	end, { menuState.disable :: any, details })

	local isDisabled = (not isPresenceEnabled) or user.status ~= "ready"
	local isLoading = user.status == "loading"
	local isError = user.status == "error"

	return e(View, {
		tag = "bg-shift-200 size-full-1400 row gap-medium padding-x-medium align-y-center radius-small",
		onActivated = if not isDisabled then toggleActive else nil,
		onSecondaryActivated = if not isDisabled then menuState.toggle else nil,
		stroke = if user.status == "error"
			then {
				Color = tokens.Color.System.Alert.Color3,
				Thickness = tokens.Stroke.Standard,
				Transparency = 0.4,
			}
			else nil,
		LayoutOrder = props.LayoutOrder,
	}, {
		Avatar = avatarImage and e(DiscordAvatar, {
			avatar = avatarImage,
			size = tokens.Size.Size_1000,
			isDimmed = isDisabled,
			LayoutOrder = nextOrder(),
		}),

		TextBlock = e(View, {
			tag = "size-0-full grow col gap-xxsmall align-y-center",
			LayoutOrder = nextOrder(),
		}, {
			Username = if isLoading
				then e(Skeleton, {
					Size = UDim2.new(0.4, 0, 0, 12),
					radius = Radius.XSmall,
					LayoutOrder = nextOrder(),
				})
				else e(Text, {
					tag = {
						["auto-xy text-label-medium"] = true,
						["content-emphasis"] = not isDisabled,
						["content-default"] = isDisabled,
					},
					Text = if details then details.username else "Unknown user",
					LayoutOrder = nextOrder(),
				}),

			StatusBlock = if isLoading
				then e(Skeleton, {
					Size = UDim2.new(0.5, 0, 0, 10),
					radius = Radius.XSmall,
					LayoutOrder = nextOrder(),
				})
				else e(View, {
					tag = "auto-xy row gap-xsmall align-y-center",
					LayoutOrder = nextOrder(),
				}, {
					Badge = e(View, {
						tag = "size-150-150 radius-circle",
						backgroundStyle = status.statusBadgeColor,
						LayoutOrder = nextOrder(),
					}),

					StatusText = e(Text, {
						tag = "auto-xy text-body-small",
						textStyle = status.statusLabelColor,
						Text = status.statusLabel,
						LayoutOrder = nextOrder(),
					}),
				}),
		}),

		RegularControls = (not isError) and e(View, {
			tag = "auto-xy row align-y-center gap-small",
			LayoutOrder = nextOrder(),
		}, {
			ActiveToggle = e(Toggle, {
				label = "",
				isChecked = isActive,
				isDisabled = isDisabled,
				onActivated = toggleActive,
				size = InputSize.Small,
				LayoutOrder = nextOrder(),
			}),

			Menu = e(Menu, {
				isOpen = menuState.enabled,
				items = menuItems,
				size = InputSize.Small,
				onPressedOutside = menuState.disable,
				onActivated = menuState.disable,
				LayoutOrder = nextOrder(),
			}, {
				MenuBtn = e(IconButton, {
					icon = IconName.ThreeDotsHorizontal,
					size = InputSize.Small,
					onActivated = menuState.toggle,
				}),
			}),
		}),

		ErrorControls = isError and e(View, {
			tag = "auto-xy row align-y-center gap-xsmall",
			LayoutOrder = nextOrder(),
		}, {
			RelinkAccount = e(Button, {
				text = "Re-link",
				variant = ButtonVariant.Utility,
				size = InputSize.Small,
				onActivated = function()
					accountStore.removeUser(props.accountId)
					ModalStore.get().open("addAccount")
				end,
				LayoutOrder = nextOrder(),
			}),

			DeleteAccount = e(IconButton, {
				icon = IconName.TrashCan,
				variant = ButtonVariant.Utility,
				size = InputSize.Small,
				onActivated = function()
					accountStore.removeUser(props.accountId)
				end,
				LayoutOrder = nextOrder(),
			}),
		}),
	})
end

return React.memo(AccountEntry)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)

local AccountEntry = require(script.Parent.AccountEntry)
local AccountStore = require(Plugin.Source.Authentication.AccountStore)
local LinkAccountEntry = require(script.Parent.LinkAccountEntry)
local NoneLinkedBlock = require(script.Parent.NoneLinkedBlock)
local Section = require(script.Parent.Parent.Section)

local createAccountStore = require(Plugin.Source.Authentication.AccountStore.createAccountStore)
type LocalUser = createAccountStore.LocalUser

local useSignalState = ReactCharm.useSignalState

local e = React.createElement
local useMemo = React.useMemo

local STATUS_ORDER = { ready = 1, error = 2, loading = 3 }

type SortedEntry = { id: string, user: LocalUser }

local sortedUsersComputed = Charm.computed(function(): { SortedEntry }
	local accountStore = AccountStore.get()
	local users = accountStore.cachedUsers()

	local entries: { SortedEntry } = {}
	for id, user in users do
		table.insert(entries, { id = id, user = user })
	end

	table.sort(entries, function(a, b)
		local aOrder = STATUS_ORDER[a.user.status] or 4
		local bOrder = STATUS_ORDER[b.user.status] or 4
		if aOrder ~= bOrder then
			return aOrder < bOrder
		end
		if a.user.status == "ready" and b.user.status == "ready" then
			return (a.user.details.globalName or "") < (b.user.details.globalName or "")
		end
		return a.id < b.id
	end)

	return entries
end)

type Props = {
	LayoutOrder: number?,
}

local function AccountList(props: Props)
	local sortedUsers = useSignalState(sortedUsersComputed)

	local children = useMemo(function(): { React.ReactNode }
		local result: { React.ReactNode } = {}

		for index, entry in sortedUsers do
			table.insert(
				result,
				e(AccountEntry, {
					key = entry.id,
					accountId = entry.id,
					localUser = entry.user,
					LayoutOrder = index,
				})
			)
		end

		if #result == 0 then
			return {
				e(NoneLinkedBlock, {
					key = "NoneLinkedBlock",
					LayoutOrder = 1,
				}),
			}
		end

		if #result < 3 then
			table.insert(
				result,
				e(LinkAccountEntry, {
					key = "LinkAccount",
					LayoutOrder = 3,
				})
			)
		end

		return result
	end, { sortedUsers })

	return e(Section, {
		title = "Linked Accounts",
		hasDivider = false,
		canToggleExpand = true,
		LayoutOrder = props.LayoutOrder,
	}, children)
end

return React.memo(AccountList)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local ModalStore = require(Plugin.Source.Modal.ModalStore)

local View = Foundation.View
local Text = Foundation.Text
local Icon = Foundation.Icon
local IconSize = Foundation.Enums.IconSize
local IconName = Foundation.Enums.IconName
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	LayoutOrder: number?,
}

local function LinkAccountEntry(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	return e(View, {
		tag = "size-full-1400 row gap-medium padding-x-medium align-y-center radius-small",
		onActivated = function()
			local modalStore = ModalStore.get()
			modalStore.open("addAccount")
		end,
		LayoutOrder = props.LayoutOrder,
	}, {
		IconBlock = e(View, {
			tag = "radius-circle row align-x-center align-y-center stroke-muted stroke-thin",
			Size = UDim2.fromOffset(tokens.Size.Size_1000, tokens.Size.Size_1000),
		}, {
			Icon = e(Icon, {
				name = IconName.PlusSmall,
				size = IconSize.Medium,
			}),
		}),

		Label = e(Text, {
			tag = "auto-xy text-label-medium content-muted",
			Text = "Link another account",
			LayoutOrder = nextOrder(),
		}),
	})
end

return React.memo(LinkAccountEntry)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local ModalStore = require(Plugin.Source.Modal.ModalStore)

local View = Foundation.View
local Text = Foundation.Text
local Button = Foundation.Button
local ButtonVariant = Foundation.Enums.ButtonVariant
local IconName = Foundation.Enums.IconName
local InputSize = Foundation.Enums.InputSize

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	LayoutOrder: number?,
}

local function NoneLinkedBlock(props: Props)
	local nextOrder = createNextOrder()

	return e(View, {
		tag = "bg-shift-100 size-full-0 auto-y col gap-medium align-x-center align-y-center padding-xlarge stroke-standard stroke-muted radius-small",
		LayoutOrder = props.LayoutOrder,
	}, {
		TextBlock = e(View, {
			tag = "auto-xy col gap-xsmall",
			LayoutOrder = nextOrder(),
		}, {
			Title = e(Text, {
				tag = "size-full-0 auto-y text-title-medium content-emphasis text-wrap text-align-x-center",
				Text = "Show what you're working on",
				LayoutOrder = nextOrder(),
			}),

			Subtitle = e(Text, {
				tag = "size-full-0 auto-y text-body-small content-default text-wrap text-align-x-center",
				Text = "Link your Discord account to display Studio activity on your profile.",
				LayoutOrder = nextOrder(),
			}),
		}),

		CtaBtn = e(Button, {
			text = "Link Discord Account",
			icon = IconName.Discord,
			variant = ButtonVariant.SoftEmphasis,
			size = InputSize.Small,
			onActivated = function()
				local modalStore = ModalStore.get()
				modalStore.open("addAccount")
			end,
			LayoutOrder = nextOrder(),
		}),
	})
end

return NoneLinkedBlock


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)

local createAccountStore = require(Plugin.Source.Authentication.AccountStore.createAccountStore)
type LocalUser = createAccountStore.LocalUser

local useTokens = Foundation.Hooks.useTokens

local PROBLEM_LABELS = {
	unauthorized = "Session expired",
	network = "Network error",
	unexpected = "Unexpected error",
}

type Status = {
	statusLabel: string,
	statusLabelColor: Foundation.ColorStyle,
	statusBadgeColor: Foundation.ColorStyle,
}

local function useAccountStatus(isActive: boolean, isPresenceEnabled: boolean, user: LocalUser): Status
	local tokens = useTokens()

	if user.status == "error" then
		local errorLabel = PROBLEM_LABELS[user.problemType] or user.problemType
		return {
			statusLabel = errorLabel,
			statusLabelColor = tokens.Color.Content.Muted,
			statusBadgeColor = tokens.Color.System.Alert,
		}
	else
		if not isPresenceEnabled then
			return {
				statusLabel = "Plugin Paused",
				statusLabelColor = tokens.Color.Content.Muted,
				statusBadgeColor = tokens.Color.Content.Muted,
			}
		elseif isActive then
			return {
				statusLabel = "Sending presence",
				statusLabelColor = tokens.Color.Content.Default,
				statusBadgeColor = tokens.Color.System.Success,
			}
		else
			return {
				statusLabel = "Paused",
				statusLabelColor = tokens.Color.Content.Muted,
				statusBadgeColor = tokens.Color.Content.Muted,
			}
		end
	end
end

return useAccountStatus


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)

local AuthStore = require(Plugin.Source.Authentication.AuthStore)
local LocalStorageStore = require(Plugin.Source.Plugin.LocalStorageStore)
local ModalStore = require(Plugin.Source.Modal.ModalStore)
local NotificationStore = require(Plugin.Source.Notification.NotificationStore)
local PluginStore = require(Plugin.Source.Plugin.PluginStore)
local UserSettingsStore = require(Plugin.Source.UserSettings.UserSettingsStore)

local Menu = Foundation.Menu
local IconButton = Foundation.IconButton
local IconName = Foundation.Enums.IconName
local InputSize = Foundation.Enums.InputSize

local STORAGE_KEYS = { "PresenceAuthStore", "PresenceLocalStorage", "PresenceUserSettings" }

local e = React.createElement
local useState = React.useState
local useMemo = React.useMemo

type Props = {
	LayoutOrder: number?,
}

local function DeveloperMenu(props: Props)
	local isOpen, setIsOpen = useState(false)
	local items = useMemo(function(): { Foundation.MenuItems }
		return {
			{
				id = "log_storage",
				text = "Print Plugin Storage",
				onActivated = function()
					local divider = string.rep("=", 16)

					local userSettings = UserSettingsStore.get().getStorage()
					local localStorage = LocalStorageStore.get().getStorage()
					local auth = AuthStore.get().getStorage()

					print(`\n{divider}\n\nPlugin storage:\n`)

					print("UserSettings:", userSettings)
					print("LocalStorage:", localStorage)
					print("AuthStore (WARNING contains access tokens):", auth)

					print(`\n{divider}`)

					setIsOpen(false)
				end,
			},
			{
				id = "clear_storage",
				text = "Clear All Saved Data",
				onActivated = function()
					setIsOpen(false)

					task.spawn(function()
						local confirmed = ModalStore.get().openAsync("confirmation", {
							props = {
								title = "Clear all saved data?",
								body = "This will remove all linked accounts, settings, and cached data. The plugin will need to be reloaded.",
							},
						})

						if not confirmed then
							return
						end

						local plugin = PluginStore.get().getPlugin()
						if not plugin then
							return
						end

						for _, key in STORAGE_KEYS do
							pcall(plugin.SetSetting, plugin, key, nil)
						end

						NotificationStore.get().push("info", "All saved data cleared — reload the plugin")
					end)
				end,
			},
		}
	end, {})

	return e(Menu, {
		isOpen = isOpen,
		items = items :: any,
		size = InputSize.Small,
		onPressedOutside = function()
			setIsOpen(false)
		end,
		onActivated = function()
			setIsOpen(false)
		end,
		LayoutOrder = props.LayoutOrder,
	}, {
		DeveloperMenuButton = e(IconButton, {
			icon = IconName.HammerCode,
			size = InputSize.XSmall,
			onActivated = function()
				setIsOpen(true)
			end,
		}),
	})
end

return DeveloperMenu


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local BuildVars = require(Plugin.Source.BuildVars)
local DeveloperMenu = require(script.Parent.DeveloperMenu)
local ModalStore = require(Plugin.Source.Modal.ModalStore)
local VersionTag = require(script.Parent.VersionTag)

local View = Foundation.View
local IconButton = Foundation.IconButton
local IconName = Foundation.Enums.IconName
local InputSize = Foundation.Enums.InputSize
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	LayoutOrder: number?,
}

local function Footer(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	return e(View, {
		tag = "size-full-900 bg-surface-100 col",
		LayoutOrder = props.LayoutOrder,
	}, {
		Divider = e(View, {
			backgroundStyle = tokens.Color.Stroke.Muted,
			Size = UDim2.new(1, 0, 0, 1),
			LayoutOrder = nextOrder(),
		}),

		LayoutBlock = e(View, {
			tag = "size-full row padding-x-large",
			LayoutOrder = nextOrder(),
		}, {
			LeftBlock = e(View, {
				tag = "size-0-full grow row gap-medium align-y-center",
				LayoutOrder = nextOrder(),
			}, {
				VersionTag = e(VersionTag, {
					LayoutOrder = nextOrder(),
				}),
			}),

			RightBlock = e(View, {
				tag = "size-0-full auto-x row gap-medium align-y-center",
				LayoutOrder = nextOrder(),
			}, {
				DeveloperMenu = BuildVars.build.isDev and e(DeveloperMenu, {
					LayoutOrder = nextOrder(),
				}),

				Settings = e(IconButton, {
					icon = IconName.Gear,
					size = InputSize.XSmall,
					onActivated = function()
						ModalStore.get().open("settings")
					end,
					LayoutOrder = nextOrder(),
				}),
			}),
		}),
	})
end

return Footer


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local BackendApi = require(Plugin.Source.Api.Backend)
local BuildVars = require(Plugin.Source.BuildVars)

local View = Foundation.View
local Text = Foundation.Text
local Badge = Foundation.Badge
local BadgeVariant = Foundation.Enums.BadgeVariant
local IconName = Foundation.Enums.IconName

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

type Props = {
	LayoutOrder: number?,
}

local function VersionTag(props: Props)
	local nextOrder = createNextOrder()

	local updateAvailable, setUpdateAvailable = React.useState(false)

	React.useEffect(function()
		-- Don't pester developers with update prompts for their own dev builds.
		if BuildVars.build.isDev then
			return
		end

		local cancelled = false

		task.spawn(function()
			local backend = BackendApi.new(BuildVars.api)
			local result = backend.getLatestVersionAsync()
			if cancelled or not result.ok then
				return
			end

			if result.value.version ~= BuildVars.build.version then
				setUpdateAvailable(true)
			end
		end)

		return function()
			cancelled = true
		end
	end, {})

	return e(View, {
		tag = "auto-xy row align-y-center gap-small",
		LayoutOrder = props.LayoutOrder,
	}, {
		VersionLabel = e(Text, {
			tag = "auto-xy text-label-small content-muted",
			Text = `v{BuildVars.build.version} ({BuildVars.build.hash})`,
			LayoutOrder = nextOrder(),
		}),

		DevBadge = BuildVars.build.isDev and e(Badge, {
			icon = IconName.HammerCode,
			text = "Dev Build",
			variant = BadgeVariant.Neutral,
			LayoutOrder = nextOrder(),
		}),

		UpdateBadge = updateAvailable and e(Badge, {
			icon = IconName.CloudArrowDown,
			text = "Update Available",
			variant = BadgeVariant.Warning,
			LayoutOrder = nextOrder(),
		}),
	})
end

return VersionTag


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)
local ReactUtils = require(Packages.ReactUtils)

local PluginWordmark = require(Plugin.Source.Common.PluginWordmark)
local PresenceManager = require(Plugin.Source.PresenceManager)
local Telemetry = require(Plugin.Source.Telemetry.fireEventAsync)
local Types = require(Plugin.Source.Telemetry.Types)

local View = Foundation.View
local Text = Foundation.Text
local Toggle = Foundation.Toggle
local InputSize = Foundation.Enums.InputSize
local InputPlacement = Foundation.Enums.InputPlacement
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder
local useSignalState = ReactCharm.useSignalState

local e = React.createElement
local useCallback = React.useCallback

type Props = {
	LayoutOrder: number?,
}

local function Header(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	local presenceManager = PresenceManager.get()
	local isActive = useSignalState(presenceManager.getIsActive)

	local toggleActive = useCallback(function()
		local newState = not presenceManager.getIsActive()
		presenceManager.setIsActive(newState)
		Telemetry.fireEvent({
			type = "presenceToggled",
			value = Types.PresenceToggled.new({ isActive = newState }),
		})
	end, { presenceManager })

	return e(View, {
		tag = "size-full-0 auto-y bg-surface-100 col gap-medium",
		LayoutOrder = props.LayoutOrder,
		ZIndex = 2,
	}, {
		LayoutBlock = e(View, {
			tag = "size-full-0 auto-y row align-y-center padding-x-large padding-top-medium gap-medium",
			LayoutOrder = nextOrder(),
		}, {
			Wordmark = e(PluginWordmark, {
				size = "small",
				LayoutOrder = nextOrder(),
			}),

			Title = e(Text, {
				tag = "auto-y grow text-align-x-left text-title-small content-muted",
				Text = "",
				LayoutOrder = nextOrder(),
			}),

			ActiveToggle = e(Toggle, {
				label = if isActive then "Active" else "Inactive",
				isChecked = isActive,
				onActivated = toggleActive,
				size = InputSize.Small,
				placement = InputPlacement.End,
				LayoutOrder = nextOrder(),
			}),
		}),

		Divider = e(View, {
			backgroundStyle = tokens.Color.Stroke.Muted,
			Size = UDim2.new(1, 0, 0, 1),
			LayoutOrder = nextOrder(),
		}),
	})
end

return React.memo(Header)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local AccountList = require(script.Parent.AccountList)
local Footer = require(script.Parent.Footer)
local Header = require(script.Parent.Header)
local PreviewBlock = require(script.Parent.PreviewBlock)
local ProfileSelection = require(script.Parent.ProfileSelection)

local View = Foundation.View
local ScrollView = Foundation.ScrollView
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement

local function MainScreen()
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	return e(View, {
		tag = "size-full bg-surface-100 col",
	}, {
		Header = e(Header, {
			LayoutOrder = nextOrder(),
		}),

		ContentsScroller = e(ScrollView, {
			tag = "size-full-0 grow",
			scroll = {
				AutomaticCanvasSize = Enum.AutomaticSize.Y,
				CanvasSize = UDim2.fromScale(0, 0),
			},
			LayoutOrder = nextOrder(),
		}, {
			LayoutContainer = e(View, {
				tag = "size-full-0 auto-y col",
				AnchorPoint = Vector2.new(0.5, 0),
				Position = UDim2.fromScale(0.5, 0),
				sizeConstraint = {
					MaxSize = Vector2.new(620, math.huge),
				},
				padding = Vector2.new(0, tokens.Padding.Medium),
			}, {
				PreviewBlock = e(PreviewBlock, {
					LayoutOrder = nextOrder(),
				}),

				ProfileSelection = e(ProfileSelection, {
					LayoutOrder = nextOrder(),
				}),

				AccountList = e(AccountList, {
					LayoutOrder = nextOrder(),
				}),
			}),
		}),

		Footer = e(Footer, {
			LayoutOrder = nextOrder(),
		}),
	})
end

return React.memo(MainScreen)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)
local ReactUtils = require(Packages.ReactUtils)

local ActivityPreview = require(Plugin.Source.ActivityPreview)
local ProfileStore = require(Plugin.Source.PresenceManager.ProfileStore)

local View = Foundation.View

local createNextOrder = ReactUtils.createNextOrder
local useSignalState = ReactCharm.useSignalState

local e = React.createElement

type Props = {
	LayoutOrder: number?,
}

local function PreviewBlock(props: Props)
	local nextOrder = createNextOrder()

	local profileStore = ProfileStore.get()
	local currentActivity = useSignalState(profileStore.getResolvedActivity)

	return e(View, {
		tag = "size-full-0 auto-y col align-x-center",
		LayoutOrder = props.LayoutOrder,
	}, {
		PreviewContainer = currentActivity and e(View, {
			tag = "auto-y",
			Size = UDim2.fromScale(0.6, 0),
			sizeConstraint = {
				MaxSize = Vector2.new(280, math.huge),
			},
			LayoutOrder = nextOrder(),
		}, {
			Preview = e(ActivityPreview, {
				activity = currentActivity,
				LayoutOrder = nextOrder(),
			}),
		}),
	})
end

return React.memo(PreviewBlock)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)
local ReactUtils = require(Packages.ReactUtils)

local ProfileStore = require(Plugin.Source.PresenceManager.ProfileStore)
local Section = require(script.Parent.Section)
local Telemetry = require(Plugin.Source.Telemetry.fireEventAsync)
local Types = require(Plugin.Source.Telemetry.Types)

local View = Foundation.View
local Dropdown = Foundation.Dropdown
local Button = Foundation.Button
local Tooltip = Foundation.Tooltip
local InputSize = Foundation.Enums.InputSize
local IconName = Foundation.Enums.IconName
local PopoverSide = Foundation.Enums.PopoverSide
local PopoverAlign = Foundation.Enums.PopoverAlign

type IconName = Foundation.IconName

local createNextOrder = ReactUtils.createNextOrder
local useSignalState = ReactCharm.useSignalState

local e = React.createElement

local profileDropdownItems = Charm.computed(function(): Foundation.DropdownItems
	type Record = { id: string, name: string, icon: IconName }
	local presetProfiles: { Record } = {}
	local customProfiles: { Record } = {}

	local allProfiles = ProfileStore.get().getAllProfiles()
	for id, profile in allProfiles do
		local target: { Record } = if profile.isPreset then presetProfiles else customProfiles
		table.insert(target, {
			id = id,
			icon = profile.icon,
			name = profile.name,
		})
	end

	-- Sort the list of profiles
	table.sort(presetProfiles, function(a, b)
		return a.name < b.name
	end)
	table.sort(customProfiles, function(a, b)
		return a.name < b.name
	end)

	-- Build the dropdown items list
	local presetItems: Foundation.BaseMenuItemGroup<Foundation.DropdownItem> = { items = {} }
	for _, profile in presetProfiles do
		table.insert(presetItems.items, {
			id = profile.id,
			text = profile.name,
			icon = profile.icon :: any,
		})
	end

	local customItems: Foundation.BaseMenuItemGroup<Foundation.DropdownItem>
	if #customProfiles > 0 then
		customItems = { items = {} }
		for _, profile in customProfiles do
			table.insert(customItems.items, {
				id = profile.id,
				text = profile.name,
				icon = profile.icon :: any,
			})
		end
	end

	-- Combine them
	local items: Foundation.DropdownItems = { presetItems }
	if customItems then
		table.insert(items, customItems)
	end

	return items
end)

type Props = {
	LayoutOrder: number?,
}

local function ProfileSelection(props: Props)
	local nextOrder = createNextOrder()

	local profileStore = ProfileStore.get()
	local activeProfileId = useSignalState(profileStore.getActiveProfileId)

	local items = useSignalState(profileDropdownItems)

	return e(Section, {
		title = "Profile",
		hasDivider = true,
		canToggleExpand = false,
		LayoutOrder = props.LayoutOrder,
	}, {
		LayoutBlock = e(View, {
			tag = "size-full-0 auto-y row gap-small align-y-center",
			LayoutOrder = nextOrder(),
		}, {
			-- We have to wrap the dropdown in a flex grow container because
			-- dropdown doesn't expose any fill behavior prop :(
			DropdownContainer = e(View, {
				tag = "auto-y grow",
				LayoutOrder = nextOrder(),
			}, {
				ProfileSelector = e(Dropdown.Root, {
					label = "",
					value = activeProfileId,
					items = items,
					size = InputSize.Small,
					width = UDim.new(1, 0),
					onItemChanged = function(id)
						if type(id) == "string" then
							profileStore.setActiveProfile(id)
							Telemetry.fireEvent({
								type = "profileSelected",
								value = Types.ProfileSelected.new({ profile = id }),
							})
						end
					end,
					LayoutOrder = nextOrder(),
				}),
			}),

			Tooltip = e(Tooltip, {
				title = "Custom profiles are coming soon!",
				align = PopoverAlign.Center,
				side = PopoverSide.Left,
				LayoutOrder = nextOrder(),
			}, {
				EditBtn = e(Button, {
					text = "Edit",
					icon = IconName.Pencil,
					size = InputSize.Small,
					isDisabled = true,
					onActivated = function() end,
					LayoutOrder = nextOrder(),
				}),
			}),
		}),
	})
end

return ProfileSelection


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactUtils = require(Packages.ReactUtils)

local View = Foundation.View
local Text = Foundation.Text
local Icon = Foundation.Icon
local IconName = Foundation.Enums.IconName
local IconSize = Foundation.Enums.IconSize
local useTokens = Foundation.Hooks.useTokens

local createNextOrder = ReactUtils.createNextOrder

local e = React.createElement
local useState = React.useState

type Props = {
	title: string,
	hasDivider: boolean,
	canToggleExpand: boolean,
	LayoutOrder: number?,
	children: React.ReactNode,
}

local function Section(props: Props)
	local nextOrder = createNextOrder()
	local tokens = useTokens()

	local isExpanded, setExpanded = useState(true)

	return e(View, {
		tag = "size-full-0 auto-y col",
		LayoutOrder = props.LayoutOrder,
	}, {
		LayoutContainer = e(View, {
			tag = "size-full-0 auto-y col gap-small padding-x-small padding-y-medium",
			LayoutOrder = nextOrder(),
		}, {
			HeaderBlock = e(View, {
				tag = "size-full-0 auto-y row radius-small align-y-center padding-x-small padding-y-xsmall",
				LayoutOrder = nextOrder(),
				onActivated = if props.canToggleExpand
					then function()
						setExpanded(function(prev)
							return not prev
						end)
					end
					else nil,
			}, {
				Title = e(Text, {
					tag = "auto-y grow text-align-x-left text-title-small content-muted",
					Text = props.title,
					LayoutOrder = nextOrder(),
				}),

				ExpandedIcon = props.canToggleExpand and e(Icon, {
					name = if isExpanded then IconName.ChevronLargeDown else IconName.ChevronLargeUp,
					size = IconSize.XSmall,
					LayoutOrder = nextOrder(),
				}),
			}),

			Container = e(View, {
				tag = "size-full-0 auto-y col gap-xsmall padding-x-small",
				LayoutOrder = nextOrder(),
				-- Some of the contents are heavy to re-render, so just make the
				-- container invisible instead of unmounting.
				-- TODO: Move to React Activity once available.
				Visible = isExpanded,
			}, props.children),
		}),

		Divider = props.hasDivider and e(View, {
			backgroundStyle = tokens.Color.Stroke.Muted,
			Size = UDim2.new(1, 0, 0, 1),
			LayoutOrder = nextOrder(),
		}),
	})
end

return React.memo(Section)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)

local Dialog = Foundation.Dialog
local DialogSize = Foundation.Enums.DialogSize
local ButtonVariant = Foundation.Enums.ButtonVariant

local e = React.createElement

type Props = {
	title: string?,
	body: string?,
	onClose: (result: boolean) -> (),
	LayoutOrder: number?,
}

local function ConfirmationDialog(props: Props)
	return e(Dialog.Root, {
		size = DialogSize.Small,
		hasBackdrop = true,
		disablePortal = false,
		onClose = function()
			props.onClose(false)
		end,
	}, {
		DialogTitle = e(Dialog.Title, {
			text = props.title or "Confirm Action",
		}),
		DialogContent = e(Dialog.Content, nil, {
			DialogText = e(Dialog.Text, {
				Text = props.body or "Are you sure you want to proceed with this action?",
			}),
		}),
		DialogActions = e(Dialog.Actions, {
			actions = {
				{
					text = "Cancel",
					variant = ButtonVariant.Standard,
					onActivated = function()
						props.onClose(false)
					end,
				} :: any,
				{
					text = "Confirm",
					variant = ButtonVariant.Emphasis,
					onActivated = function()
						props.onClose(true)
					end,
				},
			},
		}),
	})
end

return ConfirmationDialog


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)

local AddAccountFlow = require(Plugin.Source.AddAccountFlow)
local ConfirmationDialog = require(script.Parent.ConfirmationDialog)
local ModalStore = require(script.Parent.ModalStore)
local SettingsModal = require(Plugin.Source.UserSettings.SettingsModal)
local WelcomeDialog = require(Plugin.Source.Telemetry.TelemetryOptOutDialog)

local useSignalState = ReactCharm.useSignalState

local e = React.createElement

type ModalConfig = {
	type: "sheet" | "dialog",
	render: (isOpen: boolean, onClose: (result: any?) -> (), props: { [string]: any }?) -> React.ReactNode,
}

local MODAL_REGISTRY: { [ModalStore.ModalId]: ModalConfig } = {
	addAccount = {
		type = "sheet",
		render = function(isOpen, onClose, _props)
			return e(AddAccountFlow, {
				isOpen = isOpen,
				onClose = onClose,
			})
		end,
	},

	welcome = {
		type = "dialog",
		render = function(_isOpen, onClose, _props)
			return e(WelcomeDialog, {
				onClose = onClose,
			})
		end,
	},

	confirmation = {
		type = "dialog",
		render = function(_isOpen, onClose, props)
			return e(ConfirmationDialog, {
				title = props.title,
				body = props.body,
				onClose = onClose,
			})
		end,
	},

	settings = {
		type = "sheet",
		render = function(isOpen, onClose, _props)
			return e(SettingsModal, {
				isOpen = isOpen,
				onClose = onClose,
			})
		end,
	},
}

local function ModalRenderer()
	local modalStore = ModalStore.get()
	local activeModal = useSignalState(modalStore.getActiveModal)
	local activeProps = useSignalState(modalStore.getActiveProps)

	local children: { [string]: React.ReactNode } = {}

	for id: ModalStore.ModalId, config in MODAL_REGISTRY do
		local isOpen = (activeModal == id)

		if config.type == "sheet" then
			-- Sheet modals are always mounted; isOpen toggles visibility
			local function onClose(result: any?)
				modalStore.close(id, result)
			end

			children[id] = config.render(isOpen, onClose, if isOpen then activeProps else nil)
		elseif config.type == "dialog" then
			-- Dialog modals are only mounted when active
			if isOpen then
				local function onClose(result: any?)
					modalStore.close(id, result)
				end

				children[id] = config.render(isOpen, onClose, activeProps)
			end
		end
	end

	return e(React.Fragment, nil, children)
end

return ModalRenderer


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local signal = Charm.signal
local computed = Charm.computed

export type ModalId = "addAccount" | "welcome" | "confirmation" | "settings"

export type OpenOptions = {
	priority: number?,
	props: { [string]: any }?,
}

type QueueEntry = {
	id: ModalId,
	priority: number,
	props: { [string]: any }?,
	waitingThread: thread?,
}

export type ModalStore = {
	getActiveModal: () -> ModalId?,
	getActiveProps: () -> { [string]: any }?,

	open: (id: ModalId, options: OpenOptions?) -> (),
	openAsync: (id: ModalId, options: OpenOptions?) -> any,
	close: (id: ModalId?, result: any?) -> (),
}

local function createModalStore(): ModalStore
	local getQueue, setQueue = signal<<{ QueueEntry }>>({})

	local getActiveModal = computed(function(): ModalId?
		local queue = getQueue()
		local first = queue[1]
		return if first then first.id else nil
	end)

	local getActiveProps = computed(function(): { [string]: any }?
		local queue = getQueue()
		local first = queue[1]
		return if first then first.props else nil
	end)

	-- Find the insertion index that maintains priority-descending, FIFO order.
	-- Entries with higher priority come first; within the same priority,
	-- new entries go after existing ones (FIFO).
	local function findInsertIndex(queue: { QueueEntry }, priority: number): number
		for i, entry in queue do
			if entry.priority < priority then
				return i
			end
		end
		return #queue + 1
	end

	local function open(id: ModalId, options: OpenOptions?)
		setQueue(function(prev)
			-- No-op if already queued
			for _, entry in prev do
				if entry.id == id then
					return prev
				end
			end

			local priority = if options then (options.priority or 0) else 0
			local props = if options then options.props else nil

			local new = table.clone(prev)
			local index = findInsertIndex(new, priority)
			table.insert(new, index, {
				id = id,
				priority = priority,
				props = props,
			})
			return new
		end)
	end

	local function openAsync(id: ModalId, options: OpenOptions?): any
		local callingThread = coroutine.running()

		setQueue(function(prev)
			-- No-op if already queued
			for _, entry in prev do
				if entry.id == id then
					return prev
				end
			end

			local priority = if options then (options.priority or 0) else 0
			local props = if options then options.props else nil

			local new = table.clone(prev)
			local index = findInsertIndex(new, priority)
			table.insert(new, index, {
				id = id,
				priority = priority,
				props = props,
				waitingThread = callingThread,
			})
			return new
		end)

		-- Yield until close() resumes us with the result
		return coroutine.yield()
	end

	local function close(id: ModalId?, result: any?)
		local threadToResume: thread? = nil

		setQueue(function(prev)
			local targetId = id
			if targetId == nil and prev[1] then
				targetId = prev[1].id
			end

			if targetId == nil then
				return prev
			end

			local new = {}
			for _, entry in prev do
				if entry.id == targetId then
					threadToResume = entry.waitingThread
				else
					table.insert(new, entry)
				end
			end

			return new
		end)

		if threadToResume then
			task.spawn(threadToResume, result)
		end
	end

	return {
		getActiveModal = getActiveModal :: any,
		getActiveProps = getActiveProps,

		open = open,
		openAsync = openAsync,
		close = close,
	}
end

return createModalStore


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local createModalStore = require(script.createModalStore)
export type ModalId = createModalStore.ModalId

return {
	get = Charm.computed(createModalStore),
}


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactFlow = require(Packages.ReactFlow)

local Snackbar = Foundation.Snackbar
local IconName = Foundation.Enums.IconName

local useTween = ReactFlow.useTween

local e = React.createElement
local useCallback = React.useCallback
local useEffect = React.useEffect
local useRef = React.useRef

export type NotificationType = "info" | "error"

local TWEEN_INFO = TweenInfo.new(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)

type Props = {
	type: NotificationType,
	text: string,
	onClose: () -> (),
	LayoutOrder: number?,
}

local function Notification(props: Props)
	local alpha: any, updateAlpha = useTween({
		start = 0,
		info = TWEEN_INFO,
	})

	local animDelayRef = useRef(nil :: thread?)

	local onClose = useCallback(function()
		updateAlpha({
			target = 0,
			info = TWEEN_INFO,
		})

		-- Wait for the animation to complete, then tell the container to close
		local delayThread = task.delay(0.1, props.onClose)
		animDelayRef.current = delayThread
	end, { updateAlpha, props.onClose })

	-- Animate on entry
	useEffect(function()
		updateAlpha({
			target = 1,
			info = TWEEN_INFO,
		})

		local thread = task.delay(4, onClose)
		return function()
			task.cancel(thread)
			if animDelayRef.current then
				pcall(task.cancel, animDelayRef.current)
				animDelayRef.current = nil
			end
		end
	end, { updateAlpha :: any, onClose })

	return e(Snackbar, {
		AnchorPoint = Vector2.new(0.5, 1),
		Position = alpha:map(function(t: number)
			local offscreenAmount = 46
			local onscreenOffset = 8

			return UDim2.new(0.5, 0, 1, math.lerp(offscreenAmount, -onscreenOffset, t))
		end),
		title = props.text,
		icon = if props.type == "info" then IconName.CircleI else IconName.TriangleExclamation,
		onClose = onClose,
	})
end

return Notification


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local Notification = require(Plugin.Source.Notification.Notification)

local signal = Charm.signal
local computed = Charm.computed

export type NotificationType = Notification.NotificationType

type QueueEntry = {
	id: number,
	type: NotificationType,
	text: string,
}

export type NotificationStore = {
	getActive: () -> QueueEntry?,
	push: (type: NotificationType, text: string) -> (),
	pop: () -> (),
}

local function createNotificationStore(): NotificationStore
	local getQueue, setQueue = signal<<{ QueueEntry }>>({})
	local nextId = 0

	local getActive = computed(function(): QueueEntry?
		local queue = getQueue()
		return queue[1]
	end)

	local function push(type: NotificationType, text: string)
		nextId += 1
		local id = nextId

		setQueue(function(prev)
			local new = table.clone(prev)
			table.insert(new, {
				id = id,
				type = type,
				text = text,
			})
			return new
		end)
	end

	local function pop()
		setQueue(function(prev)
			if #prev == 0 then
				return prev
			end

			local new = {}
			for i = 2, #prev do
				table.insert(new, prev[i])
			end
			return new
		end)
	end

	return {
		getActive = getActive :: any,
		push = push,
		pop = pop,
	}
end

return createNotificationStore


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local createNotificationStore = require(script.createNotificationStore)
export type NotificationType = createNotificationStore.NotificationType

return {
	get = Charm.computed(createNotificationStore),
}


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)
local ReactCharm = require(Packages.ReactCharm)

local Notification = require(script.Parent.Notification)
local NotificationStore = require(script.Parent.NotificationStore)

local View = Foundation.View

local useSignalState = ReactCharm.useSignalState

local e = React.createElement
local useCallback = React.useCallback

local function NotificationsContainer()
	local notificationStore = NotificationStore.get()
	local active = useSignalState(notificationStore.getActive)

	local onClose = useCallback(function()
		notificationStore.pop()
	end, { notificationStore })

	return e(View, {
		tag = "size-full",
		ZIndex = 10,
	}, active and e(Notification, {
		key = tostring(active.id),
		type = active.type,
		text = active.text,
		onClose = onClose,
	}))
end

return React.memo(NotificationsContainer)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local createPlaceContextStore = require(script.createPlaceContextStore)

export type PlaceType = createPlaceContextStore.PlaceType
export type Status = createPlaceContextStore.Status
export type OnlinePlaceDetails = createPlaceContextStore.OnlinePlaceDetails
export type OfflinePlaceDetails = createPlaceContextStore.OfflinePlaceDetails
export type PlaceContext = createPlaceContextStore.PlaceContext

return table.freeze({
	get = Charm.computed(function()
		return createPlaceContextStore()
	end),
})


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local t = require(Packages.t)

local createPluginSettingsStore = require(script.Parent.createPluginSettingsStore)

local DiscordApi = require(Plugin.Source.Api.Discord)
type ApiUser = DiscordApi.User

type CustomProfileActivityAssets = {
	largeImage: string?,
	largeText: string?,
	smallImage: string?,
	smallText: string?,
}

type CustomProfileActivity = {
	name: string,
	details: string?,
	detailsUrl: string?,
	state: string?,
	stateUrl: string?,
	startedAt: number?,
	assets: CustomProfileActivityAssets?,
}

export type CustomProfileData = {
	name: string,
	activity: CustomProfileActivity,
}

export type CustomProfiles = { [string]: CustomProfileData }

export type LocalStorageStore = {
	hasCompletedOnboarding: boolean,
	isPresenceActive: boolean?,
	selectedProfileId: string?,
	customProfiles: CustomProfiles?,
	userDetailsCache: { [string]: ApiUser }?,
	disabledAccountIds: { [string]: true }?,
}

local defaultValue: LocalStorageStore = {
	hasCompletedOnboarding = false,
	isPresenceActive = true,
	selectedProfileId = nil,
	customProfiles = {},
	userDetailsCache = {},
	disabledAccountIds = {},
}

local validateActivityAssets = t.interface({
	largeImage = t.optional(t.string),
	largeText = t.optional(t.string),
	smallImage = t.optional(t.string),
	smallText = t.optional(t.string),
})

local validateActivity = t.interface({
	name = t.string,
	details = t.optional(t.string),
	detailsUrl = t.optional(t.string),
	state = t.optional(t.string),
	stateUrl = t.optional(t.string),
	startedAt = t.optional(t.number),
	assets = t.optional(validateActivityAssets),
})

local validateCustomProfile = t.interface({
	name = t.string,
	activity = validateActivity,
})

local validate = t.interface({
	hasCompletedOnboarding = t.boolean,
	isPresenceActive = t.optional(t.boolean),
	selectedProfileId = t.optional(t.string),
	customProfiles = t.optional(t.map(t.string, validateCustomProfile)),
	userDetailsCache = t.optional(t.table),
	disabledAccountIds = t.optional(t.table),
})

return {
	get = Charm.computed(function()
		return createPluginSettingsStore("PresenceLocalStorage", defaultValue, validate)
	end),
}


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)

local LocalStorageStore = require(Plugin.Source.Plugin.LocalStorageStore)
local MainScreen = require(Plugin.Source.MainScreen)
local ModalRenderer = require(Plugin.Source.Modal.ModalRenderer)
local ModalStore = require(Plugin.Source.Modal.ModalStore)
local NotificationsContainer = require(Plugin.Source.Notification.NotificationsContainer)
local PluginWidget = require(script.Parent.PluginWidget)
local createPluginLoader = require(Plugin.Bin.createPluginLoader)

local e = React.createElement
local useCallback = React.useCallback
local useState = React.useState
local useEffect = React.useEffect

type Props = {
	pluginLoaderContext: createPluginLoader.PluginLoaderContext,
}

local function PluginApp(props: Props)
	local enabled, setEnabled = useState(true)

	local toggleEnabled = useCallback(function()
		setEnabled(function(prev)
			return not prev
		end)
	end, {})

	local onClose = useCallback(function()
		setEnabled(false)
	end, {})

	local onRestore = useCallback(function(restoreEnabled: boolean)
		setEnabled(restoreEnabled)
	end, {})

	local onWidgetEnabledChanged = useCallback(function(nowEnabled: boolean)
		setEnabled(nowEnabled)
	end, {})

	useEffect(function()
		local connection = props.pluginLoaderContext.mainButtonClickedSignal.Connect(toggleEnabled)
		return function()
			connection.Disconnect()
		end
	end, { props.pluginLoaderContext })

	useEffect(function()
		local storage = LocalStorageStore.get().getStorage()
		if not storage.hasCompletedOnboarding then
			ModalStore.get().open("welcome")
		end
	end, {})

	return e(PluginWidget, {
		dockWidget = props.pluginLoaderContext.mainDockWidget :: DockWidgetPluginGui,
		enabled = enabled,
		zIndexBehavior = Enum.ZIndexBehavior.Sibling,
		onClose = onClose,
		onWidgetRestored = onRestore,
		onEnabledChanged = onWidgetEnabledChanged,
	}, {
		NotificationsContainer = e(NotificationsContainer),
		ModalRenderer = e(ModalRenderer),
		MainScreen = e(MainScreen),
	})
end

return PluginApp


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

export type PluginStore = {
	getPlugin: () -> Plugin?,
	setPlugin: (Plugin?) -> (),
}

--[[
	This is a basic store to keep track of the Plugin instance.

	A quirk of the `plugin` keyword is that it's only defined in top-level
	scripts, like `init.server.luau`. It must be passed along by reference after
	that point, as attempting to use `plugin` in a ModuleScript will result in
	nil value. To work around this, we just toss the plugin instance into this
	store to share it around other parts of the app
]]
local function createPluginStore(): PluginStore
	local getPlugin, setPlugin = Charm.signal(nil :: Plugin?)

	return {
		getPlugin = getPlugin,
		setPlugin = setPlugin,
	}
end

return createPluginStore


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local createPluginStore = require(script.createPluginStore)

return {
	get = Charm.computed(createPluginStore),
}


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)
local ReactRoblox = require(Packages.ReactRoblox)

local useEffect = React.useEffect

export type Props = {
	dockWidget: DockWidgetPluginGui,
	title: string?,
	enabled: boolean?,
	zIndexBehavior: Enum.ZIndexBehavior?,
	onClose: () -> ()?,
	onWidgetRestored: (boolean) -> ()?,
	onEnabledChanged: (boolean) -> ()?,
	onSizeChange: (size: Vector2) -> ()?,
	children: React.ReactNode,
}

local function PluginWidget(props: Props)
	local dockWidget = props.dockWidget
	local title = props.title
	local enabled = if props.enabled == nil then true else props.enabled
	local zIndexBehavior = props.zIndexBehavior or Enum.ZIndexBehavior.Sibling
	local onClose = props.onClose
	local onWidgetRestored = props.onWidgetRestored
	local onEnabledChanged = props.onEnabledChanged
	local onSizeChange = props.onSizeChange

	useEffect(function(): (() -> ())?
		if onClose then
			dockWidget:BindToClose(onClose)
			return function()
				dockWidget:BindToClose()
			end
		end
		return
	end, { dockWidget or false, onClose or false } :: { any })

	useEffect(function()
		if title and dockWidget.Title ~= title then
			dockWidget.Title = title
			dockWidget.Name = title
		end
	end, { dockWidget or false, title } :: { any })

	useEffect(function()
		if dockWidget.Enabled ~= enabled then
			dockWidget.Enabled = enabled
		end
	end, { dockWidget or false, enabled } :: { any })

	useEffect(function()
		if dockWidget.ZIndexBehavior ~= zIndexBehavior then
			dockWidget.ZIndexBehavior = zIndexBehavior
		end
	end, { dockWidget or false, zIndexBehavior } :: { any })

	useEffect(function(): (() -> ())?
		if onSizeChange ~= nil then
			local connection = dockWidget:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
				onSizeChange(dockWidget.AbsoluteSize)
			end)
			return function()
				connection:Disconnect()
			end
		end
		return
	end, { dockWidget or false, onSizeChange or false } :: { any })

	useEffect(function(): (() -> ())?
		if onWidgetRestored and dockWidget.HostWidgetWasRestored then
			onWidgetRestored(dockWidget.Enabled)
		end
		return
	end, { dockWidget })

	useEffect(function(): (() -> ())?
		if onEnabledChanged then
			local connection = dockWidget:GetPropertyChangedSignal("Enabled"):Connect(function()
				onEnabledChanged(dockWidget.Enabled)
			end)
			return function()
				connection:Disconnect()
			end
		end
		return
	end, { dockWidget :: any, onEnabledChanged or false })

	return if dockWidget then ReactRoblox.createPortal(props.children, dockWidget) else nil
end

return PluginWidget


local HttpService = game:GetService("HttpService")

local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local Logger = require(Plugin.Source.Logger)
local NotificationStore = require(Plugin.Source.Notification.NotificationStore)
local PluginStore = require(script.Parent.PluginStore)

export type PluginSettingsStore<T> = {
	getStorage: () -> T,
	setStorage: (T | (T) -> T) -> (),

	isSettingDefault: (settingName: string) -> boolean,

	getIsLoading: () -> boolean,
	getErr: () -> string?,

	dispose: () -> (),
}

--[[
	Creates a store to manage plugin settings.

	Settings are persisted using Plugin:SetSetting and Plugin:GetSetting, and
    are stored as JSON strings. The settings are loaded once when the store is
    first created, and any changes to the settings are automatically saved.
]]
local function createPluginSettingsStore<T>(
	storageKey: string,
	defaultValue: T,
	validate: (value: any) -> (boolean, string)
): PluginSettingsStore<T>
	local getStorage, setStorage = Charm.signal(defaultValue)
	local getIsLoading, setIsLoading = Charm.signal(true)
	local getErr, setErr = Charm.signal(nil :: string?)

	local function readPluginSettingsAsync(plugin: Plugin): T?
		Logger:Info(`reading plugin settings for {storageKey} from disk...`)

		local data = plugin:GetSetting(storageKey)

		Logger:Debug(`read raw plugin settings for {storageKey}:`, data)

		if typeof(data) == "string" then
			Logger:Info(`deserializing plugin settings for {storageKey} to object...`)

			local success, result = pcall(function()
				return HttpService:JSONDecode(data)
			end)

			if not success then
				Logger:Warn(`failed to load plugin settings for {storageKey}:`, result)
				setErr(result)
				NotificationStore.get().push("error", "Failed to load plugin settings")
				return nil
			end

			local isValid, message = validate(result)

			if not isValid then
				Logger:Warn(`plugin settings are malformed for {storageKey}:`, message)
				setErr(message)
				NotificationStore.get().push("error", "Failed to load plugin settings")
				return nil
			end

			Logger:Info(`got plugin settings for {storageKey}`)

			return result
		end

		Logger:Info("no plugin settings to read")

		return nil
	end

	local function writePluginSettingsAsync(plugin: Plugin, storage: T)
		Logger:Info("writing plugin settings to disk...")
		local data = HttpService:JSONEncode(storage)

		if data then
			local success, err = pcall(function()
				plugin:SetSetting(storageKey, data)
			end)

			if not success then
				Logger:Warn(`failed to write plugin settings to disk: {err}`)
				NotificationStore.get().push("error", "Failed to save plugin settings")
			end
		end
	end

	-- Load settings from disk when plugin becomes available
	local disposeLoad = Charm.effect(function()
		local isLoading = getIsLoading()
		local plugin = PluginStore.get().getPlugin()

		if plugin and isLoading then
			task.spawn(function()
				local data = readPluginSettingsAsync(plugin)
				if data then
					setStorage(data)
				end
				setIsLoading(false)
			end)
		end
	end)

	-- Write settings to disk when storage changes.
	-- Gated behind isLoading to prevent writing defaults before the load completes.
	local prevStorage: T? = defaultValue
	local disposeWrite = Charm.effect(function()
		local storage = getStorage()
		local isLoading = getIsLoading()
		local plugin = PluginStore.get().getPlugin()

		if plugin and not isLoading and storage ~= prevStorage then
			task.spawn(function()
				writePluginSettingsAsync(plugin, storage)
			end)
		end

		prevStorage = storage
	end)

	local function dispose()
		disposeLoad()
		disposeWrite()
	end

	local function isSettingDefault(settingName: string): boolean
		local storage = getStorage()
		if typeof(defaultValue) ~= "table" or typeof(storage) ~= "table" then
			return true
		end

		local settingDefaultValue = defaultValue[settingName]
		local storedValue = storage[settingName]

		return storedValue == nil or storedValue == settingDefaultValue
	end

	return {
		getIsLoading = getIsLoading,
		getErr = getErr,
		getStorage = getStorage,
		setStorage = setStorage,
		isSettingDefault = isSettingDefault,
		dispose = dispose,
	}
end

return createPluginSettingsStore


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local React = require(Packages.React)
local ReactRoblox = require(Packages.ReactRoblox)

local ContextProviders = require(Plugin.Source.Common.ContextProviders)
local Logger = require(Plugin.Source.Logger)
local Telemetry = require(Plugin.Source.Telemetry.fireEventAsync)
local Types = require(Plugin.Source.Telemetry.Types)
local createPluginLoader = require(Plugin.Bin.createPluginLoader)

local PluginApp = require(script.Parent.PluginApp)

local e = React.createElement

local function stopwatch(callback: () -> ()): string
	local start = os.clock()
	callback()
	return ("%.2fms"):format((os.clock() - start) * 1000)
end

local function createPresencePlugin(plugin: Plugin, pluginLoaderContext: createPluginLoader.PluginLoaderContext)
	local widget = pluginLoaderContext.mainDockWidget
	assert(widget ~= nil, "Main dock widget not found")

	local root = ReactRoblox.createRoot(widget)
	local app = e(ContextProviders, {
		plugin = plugin,
		overlayGui = widget :: GuiBase2d,
	}, {
		App = e(PluginApp, {
			pluginLoaderContext = pluginLoaderContext,
		}),
	})

	local function unmount()
		Logger:Info("unmounting app...")
		local elapsedMs = stopwatch(function()
			root:unmount()
		end)
		Logger:Info(`unmounted app in {elapsedMs}`)
	end

	local function mount()
		Logger:Info("mounting app...")
		local elapsedMs = stopwatch(function()
			root:render(app)
		end)
		Logger:Info(`mounted app in {elapsedMs}`)

		Telemetry.fireEvent({ type = "uiOpened", value = Types.UiOpened.new() })
	end

	local function destroy()
		Logger:Info("destroying app...")
		local elapsedMs = stopwatch(function()
			unmount()
		end)
		Logger:Info(`destroyed app in {elapsedMs}`)
	end

	return {
		mount = mount,
		unmount = unmount,
		destroy = destroy,
	}
end

return createPresencePlugin


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)
local Foundation = require(Packages.Foundation)

local createPresenceManager = require(Plugin.Source.PresenceManager.createPresenceManager)
type ActivityConfig = createPresenceManager.ActivityConfig

local PlaceContextStore = require(Plugin.Source.PlaceContextStore)
type PlaceContext = PlaceContextStore.PlaceContext

local LocalStorageStore = require(Plugin.Source.Plugin.LocalStorageStore)
local Logger = require(Plugin.Source.Logger)
local NotificationStore = require(Plugin.Source.Notification.NotificationStore)
local RetryBackoff = require(Plugin.Source.Api.RetryBackoff)
local presets = require(script.Parent.presets)

local IconName = Foundation.Enums.IconName

local signal = Charm.signal
local effect = Charm.effect
local computed = Charm.computed
local peek = Charm.peek

local MAX_BUILD_RETRIES = 3

type CustomProfileData = LocalStorageStore.CustomProfileData
type CustomProfiles = LocalStorageStore.CustomProfiles

export type Profile = {
	name: string,
	icon: Foundation.IconName,
	isPreset: boolean,
	buildActivityAsync: (context: PlaceContext) -> ActivityConfig,
}

export type ProfileStore = {
	getActiveProfileId: () -> string,
	getActiveProfile: () -> Profile?,
	getAllProfiles: () -> { [string]: Profile },
	getResolvedActivity: () -> ActivityConfig?,

	setActiveProfile: (id: string) -> (),
	setCustomProfile: (id: string, data: CustomProfileData) -> (),
	removeCustomProfile: (id: string) -> (),

	destroy: () -> (),
}

--- Wrap a CustomProfileData into a runtime Profile.
--- The build function ignores context and returns the stored static activity.
local function wrapCustomProfile(data: CustomProfileData): Profile
	return {
		name = data.name,
		icon = IconName.DiamondSimplified :: Foundation.IconName,
		isPreset = false,
		buildActivityAsync = function(_context: PlaceContext): ActivityConfig
			return data.activity
		end,
	}
end

--- Wrap a preset into a runtime Profile.
local function wrapPreset(preset: presets.PresetProfile): Profile
	return {
		name = preset.name,
		icon = preset.icon,
		isPreset = true,
		buildActivityAsync = preset.buildActivityAsync,
	}
end

--[[
	Retry a profile build with exponential backoff.
	The build function can yield. Returns nil on persistent failure.
]]
local function buildWithRetry(profile: Profile, context: PlaceContext): ActivityConfig?
	local backoffConfig = RetryBackoff.defaultConfig()
	local backoffState = RetryBackoff.createState()

	for attempt = 1, MAX_BUILD_RETRIES do
		local success, result = pcall(profile.buildActivityAsync, context)

		if success then
			return result
		end

		if attempt == MAX_BUILD_RETRIES then
			Logger:Warn(`ProfileStore: build failed after {MAX_BUILD_RETRIES} attempts: {result}`)
			NotificationStore.get().push("error", "Failed to build presence profile")
			return nil
		end

		local delay = RetryBackoff.nextAttempt(backoffConfig, backoffState)
		Logger:Warn(
			`ProfileStore: build error, retrying in {string.format("%.1f", delay)}s (attempt {attempt}/{MAX_BUILD_RETRIES}): {result}`
		)
		task.wait(delay)
	end

	-- Unreachable, but satisfies the type checker
	return nil
end

local function createProfileStore(): ProfileStore
	local localStorage = LocalStorageStore.get()

	local getActiveProfileId = computed(function(): string
		return localStorage.getStorage().selectedProfileId or "default"
	end)

	local function setActiveProfile(id: string)
		localStorage.setStorage(function(prev)
			local new = table.clone(prev)
			new.selectedProfileId = id
			return new
		end)
	end

	local getActiveProfile = computed(function(): Profile?
		local id = getActiveProfileId()
		local customProfiles = localStorage.getStorage().customProfiles or {}

		local preset = presets[id]
		if preset then
			return wrapPreset(preset)
		end

		local custom = customProfiles[id]
		if custom then
			return wrapCustomProfile(custom)
		end

		return wrapPreset(presets.default)
	end)

	-- Reset to default when the active profile no longer exists.
	-- This is an effect rather than inline in the computed above to
	-- avoid writing to a signal during a computed derivation.
	local disposeProfileFallbackEffect = effect(function()
		local id = getActiveProfileId()
		local customProfiles = localStorage.getStorage().customProfiles or {}

		if id ~= "default" and not presets[id] and not customProfiles[id] then
			setActiveProfile("default")
		end
	end)

	local getAllProfiles = computed(function(): { [string]: Profile }
		local all: { [string]: Profile } = {}

		for id, preset in presets do
			all[id] = wrapPreset(preset)
		end

		local customProfiles: CustomProfiles = localStorage.getStorage().customProfiles or {}
		for id, custom in customProfiles do
			all[id] = wrapCustomProfile(custom)
		end

		return all
	end)

	-- Resolved activity from building the active profile against PlaceContext
	local getResolvedActivity, setResolvedActivity = signal(nil :: ActivityConfig?)
	local buildThread: thread?

	local disposeBuildEffect = effect(function()
		-- Cancel any in-flight build from a previous run
		if buildThread then
			pcall(task.cancel, buildThread)
		end

		local placeContextStore = PlaceContextStore.get()
		local context = placeContextStore.getContext()

		if context == nil then
			setResolvedActivity(nil)
			return
		end

		local profile = getActiveProfile()
		if not profile then
			setResolvedActivity(nil)
			return
		end

		buildThread = task.spawn(function()
			local activity = buildWithRetry(profile, context)
			setResolvedActivity(activity)
			buildThread = nil
		end)
	end)

	local function setCustomProfile(id: string, data: CustomProfileData)
		localStorage.setStorage(function(prev)
			local new = table.clone(prev)
			local customProfiles = table.clone(prev.customProfiles or {})
			customProfiles[id] = data
			new.customProfiles = customProfiles
			return new
		end)
	end

	local function removeCustomProfile(id: string)
		-- If the removed profile was active, fall back to default
		if peek(getActiveProfileId) == id then
			setActiveProfile("default")
		end

		localStorage.setStorage(function(prev)
			local new = table.clone(prev)
			local customProfiles = table.clone(prev.customProfiles or {})
			customProfiles[id] = nil
			new.customProfiles = customProfiles
			return new
		end)
	end

	local function destroy()
		disposeProfileFallbackEffect()
		disposeBuildEffect()

		if buildThread then
			pcall(task.cancel, buildThread)
			buildThread = nil
		end
	end

	return {
		getActiveProfileId = getActiveProfileId,
		getActiveProfile = getActiveProfile,
		getAllProfiles = getAllProfiles,
		getResolvedActivity = getResolvedActivity,

		setActiveProfile = setActiveProfile,
		setCustomProfile = setCustomProfile,
		removeCustomProfile = removeCustomProfile,

		destroy = destroy,
	}
end

return createProfileStore


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local createProfileStore = require(script.createProfileStore)

export type ProfileStore = createProfileStore.ProfileStore
export type Profile = createProfileStore.Profile

return table.freeze({
	get = Charm.computed(function()
		return createProfileStore()
	end),
})


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)

local IconName = Foundation.Enums.IconName
type IconName = Foundation.IconName

local createPresenceManager = require(Plugin.Source.PresenceManager.createPresenceManager)
type ActivityConfig = createPresenceManager.ActivityConfig

local PlaceContextStore = require(Plugin.Source.PlaceContextStore)
type PlaceContext = PlaceContextStore.PlaceContext

local STUDIO_LOGO_ASSET = "studio-logo"

export type PresetProfile = {
	name: string,
	icon: IconName,
	buildActivityAsync: (context: PlaceContext) -> ActivityConfig,
}

local presets: { [string]: PresetProfile } = {
	default = {
		name = "Default",
		icon = IconName.PhotoCameraFace :: IconName,
		buildActivityAsync = function(context: PlaceContext): ActivityConfig
			local onlineDetails = if context.placeType == "online" then context.onlinePlaceDetails else nil
			local offlineDetails = if context.placeType == "offline" then context.offlinePlaceDetails else nil

			return {
				name = "Roblox Studio",
				details = "Editing in Studio",
				state = if onlineDetails
					then onlineDetails.placeName
					elseif offlineDetails then offlineDetails.placeName
					else nil,
				stateUrl = if onlineDetails then onlineDetails.placeLink else nil,
				assets = if onlineDetails
					then {
						largeImage = onlineDetails.iconWebUrl,
						smallImage = STUDIO_LOGO_ASSET,
						smallText = "Studio Activity",
					}
					else nil,
			}
		end,
	},

	confidential = {
		name = "Confidential",
		icon = IconName.Eyelashes :: IconName,
		buildActivityAsync = function(_context: PlaceContext): ActivityConfig
			return {
				name = "Roblox Studio",
				details = "Editing in Studio",
				assets = {
					largeImage = STUDIO_LOGO_ASSET,
					largeText = "Studio Activity",
				},
			}
		end,
	},
}

return table.freeze(presets)


local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Charm = require(Packages.Charm)

local BuildVars = require(Plugin.Source.BuildVars)
local DiscordApi = require(Plugin.Source.Api.Discord)
local createPresenceManager = require(script.createPresenceManager)

export type PresenceManager = createPresenceManager.PresenceManager
export type ActivityConfig = createPresenceManager.ActivityConfig
export type ActivityConfigAssets = createPresenceManager.ActivityConfigAssets

return table.freeze({
	get = Charm.computed(function()
		local discord = DiscordApi.new({
			clientId = BuildVars.discord.clientId,
		})

		return createPresenceManager(discord)
	end),
})


export type Level = "Low" | "Medium" | "Quartile" | "High"

local NUMERIC_LEVEL: { [Level]: number } = {
	Low = 1,
	Medium = 2,
	Quartile = 3,
	High = 4,
}

local function toNumber(level: Level): number
	return NUMERIC_LEVEL[level]
end

return {
	Low = "Low" :: "Low",
	Medium = "Medium" :: "Medium",
	Quartile = "Quartile" :: "Quartile",
	High = "High" :: "High",
	toNumber = toNumber,
}


local AssetService = game:GetService("AssetService")

local Plugin = script:FindFirstAncestor("StudioActivity")

local Packages = Plugin.Packages
local Foundation = require(Packages.Foundation)
local React = require(Packages.React)

local ErrorCorrection = require(script.Parent.ErrorCorrection)
local getResolutionScale = require(Plugin.Source.Common.getResolutionScale)
local renderMatrixToImage = require(script.Parent.renderMatrixToImage)
local useQRCodeMatrix = require(script.Parent.useQRCodeMatrix)

local View = Foundation.View

local e = React.createElement

export type Props = {
	text: string,
	errorCorrection: ErrorCorrection.Level,
	size: number,
	color: Color3?,
	transparency: number?,
	LayoutOrder: number?,
}

local DEFAULT_COLOR = Color3.fromRGB(0, 0, 0)
local RESOLUTION_SCALE = getResolutionScale()

local function QRCodeDisplay(props: Props)
	local color = props.color or DEFAULT_COLOR
	local transparency = props.transparency or 0
	local imageSize = props.size

	local matrix = useQRCodeMatrix(props.text, props.errorCorrection)
	local imageRef = React.useRef(nil :: EditableImage?)
	local content, setContent = React.useState(nil :: Content?)
	local renderSize = imageSize * RESOLUTION_SCALE

	React.useEffect(function()
		local image = imageRef.current
		if not image or image.Size.X ~= renderSize then
			if image then
				image:Destroy()
			end
			image = AssetService:CreateEditableImage({
				Size = Vector2.new(renderSize, renderSize),
			})

			assert(image, "luau")
			imageRef.current = image
			setContent(Content.fromObject(image))
		end

		assert(image, "luau")
		renderMatrixToImage(image, renderSize, matrix, color, 1 - transparency)
	end, { renderSize :: any, matrix, color, transparency })

	-- Destroy the EditableImage on unmount
	React.useEffect(function()
		return function()
			if imageRef.current then
				imageRef.current:Destroy()
			end
		end
	end, {})

	return e(View, {
		Size = UDim2.fromOffset(props.size, props.size),
		LayoutOrder = props.LayoutOrder,
	}, {
		QRImage = e("ImageLabel", {
			AnchorPoint = Vector2.new(0.5, 0.5),
			BackgroundTransparency = 1,
			ImageContent = content,
			Position = UDim2.fromScale(0.5, 0.5),
			Size = UDim2.fromOffset(imageSize, imageSize),
		}),
	})
end

return QRCodeDisplay


local ErrorCorrection = require(script.ErrorCorrection)
local QRCodeDisplay = require(script.QRCodeDisplay)

export type ErrorCorrectionLevel = ErrorCorrection.Level
export type QRCodeDisplayProps = QRCodeDisplay.Props

return {
	QRCodeDisplay = QRCodeDisplay,
	ErrorCorrection = ErrorCorrection,
}


--[[
	Writes a QR code matrix as pixel data onto an EditableImage.
	Foreground modules are drawn with the given color and alpha.
	Background modules are fully transparent.
]]
local function renderMatrixToImage(
	image: EditableImage,
	imageSize: number,
	matrix: { [number]: { [number]: number } },
	color: Color3,
	alpha: number
)
	local matrixSize = #matrix
	local pixelBuffer = buffer.create(imageSize * imageSize * 4)

	local cr = math.round(color.R * 255)
	local cg = math.round(color.G * 255)
	local cb = math.round(color.B * 255)
	local ca = math.round(alpha * 255)

	local offset = 0
	for row = 0, imageSize - 1 do
		local matrixRow = matrix[math.floor(row / imageSize * matrixSize) + 1]
		for col = 0, imageSize - 1 do
			local matrixCol = math.floor(col / imageSize * matrixSize) + 1
			if matrixRow[matrixCol] > 0 then
				buffer.writeu8(pixelBuffer, offset, cr)
				buffer.writeu8(pixelBuffer, offset + 1, cg)
				buffer.writeu8(pixelBuffer, offset + 2, cb)
				buffer.writeu8(pixelBuffer, offset + 3, ca)
			else
				buffer.writeu8(pixelBuffer, offset, 255)
				buffer.writeu8(pixelBuffer, offset + 1, 255)
				buffer.writeu8(pixelBuffer, offset + 2, 255)
				buffer.writeu8(pixelBuffer, offset + 3, 0)
			end
			offset += 4
		end
	end

	image:WritePixelsBuffer(Vector2.zero, Vector2.new(imageSize, imageSize), pixelBuffer)
end

return renderMatrixToImage