-- ROBLOX FIXME LUAU: Casting to any to prevent unwanted type narrowing for write method
local BufferedConsole = setmetatable({}, { __index = Console }) :: any
BufferedConsole.__index = BufferedConsole

function BufferedConsole.new(): BufferedConsole
	local self = setmetatable(
		Console.new({
			write = function(self, message)
				BufferedConsole.write(((self :: any) :: BufferedConsolePrivate)._buffer, "log", message, nil)

				return true
			end,
		} :: Writeable),
		BufferedConsole
	) :: any
	self._buffer = {}
	self._counters = {}
	self._timers = {}
	self._groupDepth = 0
	self.Console = Console
	return self :: BufferedConsole
end

function BufferedConsole.write(buffer: ConsoleBuffer, type: LogType, message: LogMessage, level: (number | nil)?)
	local stackLevel = if level ~= nil then level else 2
	local rawStack = ErrorWithStack.new(nil, BufferedConsole.write).stack
	invariant(rawStack, "always have a stack trace")
	local origin = Array.join(
		Array.filter(Array.slice(String.split(rawStack :: string, "\n"), stackLevel), Boolean.toJSBoolean),
		"\n"
	)
	table.insert(buffer, { message = message, origin = origin, type = type })
	return buffer
end

function BufferedConsole:_log(type: LogType, message: LogMessage)
	BufferedConsole.write(self._buffer, type :: any, ("  "):rep(self._groupDepth) .. message, 3)
end

function BufferedConsole.assert(self: BufferedConsolePrivate, value: unknown, message: (string | Error)?)
	xpcall(function()
		assert(value)
	end, function(error_)
		local msg = ""
		if message ~= nil then
			msg = " " .. tostring(message)
		end

		self:_log("assert", tostring(error_) .. msg)
	end)
end

function BufferedConsole.count(self: BufferedConsolePrivate, label_: string?)
	local label: string = if label_ ~= nil then label_ else "default"
	if self._counters[label] == nil then
		self._counters[label] = 0
	end
	self._counters[label] += 1
	self:_log("count", format("%s: %s", label, self._counters[label]))
end

function BufferedConsole.countReset(self: BufferedConsolePrivate, label_: string?)
	local label: string = if label_ ~= nil then label_ else "default"
	self._counters[label] = 0
end

function BufferedConsole.debug(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("debug", format(firstArg, ...))
end

function BufferedConsole.dir(self: BufferedConsolePrivate, firstArg: unknown, options_: InspectOptions?)
	local options: InspectOptions = options_ or {}
	local representation = inspect(firstArg, options)
	self:_log("dir", formatWithOptions(options, representation))
end

function BufferedConsole.dirxml(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("dirxml", format(firstArg, ...))
end

function BufferedConsole.error(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("error", format(firstArg, ...))
end

function BufferedConsole.group(self: BufferedConsolePrivate, title: string?, ...: any)
	local rest = { ... }
	self._groupDepth += 1
	if Boolean.toJSBoolean(title) or #rest > 0 then
		self:_log("group", chalk.bold(format(title, ...)))
	end
end

function BufferedConsole.groupCollapsed(self: BufferedConsolePrivate, title: string?, ...: any)
	local rest = { ... }
	self._groupDepth += 1
	if Boolean.toJSBoolean(title) or #rest > 0 then
		self:_log("groupCollapsed", chalk.bold(format(title, ...)))
	end
end

function BufferedConsole.groupEnd(self: BufferedConsolePrivate)
	if self._groupDepth > 0 then
		self._groupDepth -= 1
	end
end

function BufferedConsole.info(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("info", format(firstArg, ...))
end

function BufferedConsole.log(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("log", format(firstArg, ...))
end

function BufferedConsole.time(self: BufferedConsolePrivate, label_: string?)
	local label: string = if label_ ~= nil then label_ else "default"
	if Boolean.toJSBoolean(self._timers[label]) then
		return
	end
	self._timers[label] = DateTime.now()
end

function BufferedConsole.timeEnd(self: BufferedConsolePrivate, label_: string?)
	local label: string = if label_ ~= nil then label_ else "default"
	local startTime = self._timers[label]
	if Boolean.toJSBoolean(startTime) then
		local endTime = DateTime.now()
		local time = endTime.UnixTimestampMillis - startTime.UnixTimestampMillis
		self:_log("time", format("%s: %s", label, formatTime(time)))
		self._timers[label] = nil
	end
end

function BufferedConsole.timeLog(self: BufferedConsolePrivate, label_: string?, ...: any)
	local label: string = if label_ ~= nil then label_ else "default"
	local startTime = self._timers[label]
	if Boolean.toJSBoolean(startTime) then
		local endTime = DateTime.now()
		local time = endTime.UnixTimestampMillis - startTime.UnixTimestampMillis
		self:_log("time", format("%s: %s", label, formatTime(time), ...))
	end
end

function BufferedConsole.warn(self: BufferedConsolePrivate, firstArg: unknown, ...: any)
	self:_log("warn", format(firstArg, ...))
end

function BufferedConsole.getBuffer(self: BufferedConsolePrivate): ConsoleBuffer?
	return if #self._buffer > 0 then self._buffer else nil
end

function invariant(condition: unknown, message: string?)
	if not Boolean.toJSBoolean(condition) then
		error(Error.new(message))
	end
end

exports.default = BufferedConsole
return exports