--!strict
local ffi = zune.ffi

local c = require("@self/c");

local glfw = c.glfw;

local window = require("@self/window");

local module = {};

module.gl = require("@self/gl");

module.D = c.defined;

function module.init(): ()
    if (glfw.glfwInit() == glfw.GLFW_FALSE) then
        module.emitError();
    end
end

function module.terminate(): ()
    glfw.glfwTerminate();
end

function module.getVersionString(): string
    return buffer.tostring(glfw.glfwGetVersionString():span());
end

function module.emitError() : never
    local errname, errdesc = module.getError();
    assert(errname and errdesc, "Failed to read error information");
    error(`{errname} - {errdesc}`)
end

function module.getError() : (string?, string?)
    local description = ffi.alloc(ffi.types.pointer:size()):release();
    local code = glfw.glfwGetError(description);
    if (code == 0) then
        return nil;
    end
    local error_name = c.ErrorCodes[code];
    return error_name, buffer.tostring(description:readptr():span());
end

function module.getPlatform(): number
    return glfw.glfwGetPlatform();
end
function module.getProcAddress(name: string): FFIPointer
    return glfw.glfwGetProcAddress(name);
end
function module.extensionSupported(extension: string): boolean
    return glfw.glfwExtensionSupported(extension) == glfw.GLFW_TRUE;
end
function module.getPlatformName(platform: number): string
    return c.Platforms[platform];
end

function module.platformSupported(platform: number): boolean
    return glfw.glfwPlatformSupported(platform) == glfw.GLFW_TRUE;
end

function module.WindowHint(hint: number, value: number): ()
    glfw.glfwWindowHint(hint, value);
end

function module.WindowHintString(hint: number, value: string): ()
    glfw.glfwWindowHintString(hint, value);
end

function module.createWindow(title: string, width: number, height: number): window.Window
    local ptr = glfw.glfwCreateWindow(width, height, title, nil, nil);
    if (ptr == ffi.null) then
        module.emitError();
    end
    return window.init(ptr);
end

function module.PollEvents(): ()
    glfw.glfwPollEvents();
end

function module.swapInterval(interval: number): ()
    glfw.glfwSwapInterval(interval);
end

return module;


--!strict
local ffi = zune.ffi;

local c = require("./c");

local glfw = c.glfw;

local window = {};
window.__index = window;

function window.init(ptr: FFIPointer)
    return setmetatable({
        handle = ptr::FFIPointer?,
        callbacks = {}::{[string]: FFIPointer?},
    }, window);
end

--[=[
    Returns the title of the specified window.
]=]
function window.getTitle(self: Window): string
    if (not self.handle) then
        error("Window handle is invalid");
    end
    return buffer.tostring(glfw.glfwGetWindowTitle(self.handle):span());
end

--[=[
    Sets the title of the specified window.
]=]
function window.setTitle(self: Window, title: string): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowTitle(self.handle, title);
end

--[=[
    Retrieves the position of the content area of the specified window.
]=]
function window.getPosition(self: Window): (number, number)
    if (not self.handle) then
        error("Window handle is invalid");
    end
    local x = ffi.alloc(ffi.types.i32:size()):release();
    local y = ffi.alloc(ffi.types.i32:size()):release();
    glfw.glfwGetWindowPos(
        self.handle,
        x,
        y
    );
    return x:readi32(), y:readi32();
end

--[=[
    Sets the position of the content area of the specified window.
]=]
function window.setPosition(self: Window, x: number, y: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowPos(self.handle, x, y);
end

--[=[
    Retrieves the size of the content area of the specified window.
]=]
function window.getSize(self: Window): (number, number)
    if (not self.handle) then
        error("Window handle is invalid");
    end
    local width = ffi.alloc(ffi.types.i32:size()):release();
    local height = ffi.alloc(ffi.types.i32:size()):release();
    glfw.glfwGetWindowSize(
        self.handle,
        width,
        height
    );
    return width:readi32(), height:readi32();
end

--[=[
    Retrieves the size of the framebuffer of the specified window.
]=]
function window.getFramebufferSize(self: Window): (number, number)
    if (not self.handle) then
        error("Window handle is invalid");
    end
    local width = ffi.alloc(ffi.types.i32:size()):release();
    local height = ffi.alloc(ffi.types.i32:size()):release();
    glfw.glfwGetFramebufferSize(
        self.handle,
        width,
        height
    );
    return width:readi32(), height:readi32();
end

--[=[
    Sets the size of the content area of the specified window.
]=]
function window.setSize(self: Window, width: number, height: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowSize(self.handle, width, height);
end

--[=[
    Sets the size limits of the specified window.
]=]
function window.setSizeLimits(self: Window, minwidth: number, minheight: number, maxwidth: number, maxheight: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowSizeLimits(self.handle, minwidth, minheight, maxwidth, maxheight);
end

--[=[
    Sets the aspect ratio of the specified window.
]=]
function window.setAspectRatio(self: Window, numer: number, denom: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowAspectRatio(self.handle, numer, denom);
end

--[=[
    Returns the opacity of the whole window.
]=]
function window.getOpacity(self: Window): number
    if (not self.handle) then
        error("Window handle is invalid");
    end
    return glfw.glfwGetWindowOpacity(self.handle);
end

--[=[
    Sets the opacity of the whole window.
]=]
function window.setOpacity(self: Window, opacity: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowOpacity(self.handle, opacity);
end

--[=[
    Restores the specified window.
]=]
function window.restore(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwRestoreWindow(self.handle);
end

--[=[
    Maximizes the specified window.
]=]
function window.maximize(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwMaximizeWindow(self.handle);
end

--[=[
    Shows the specified window.
]=]
function window.show(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwShowWindow(self.handle);
end

--[=[
    Hides the specified window.
]=]
function window.hide(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwHideWindow(self.handle);
end

--[=[
    Brings the specified window to front and sets input focus.
]=]
function window.focus(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwFocusWindow(self.handle);
end

--[=[
    Requests user attention to the specified window.
]=]
function window.requestAttention(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwRequestWindowAttention(self.handle);
end

--TODO: Implement `Monitor`
--[=[
    Retrieves the monitor that the window is in fullscreen on.
]=]
function window.getMonitor(self: Window): FFIPointer
    if (not self.handle) then
        error("Window handle is invalid");
    end
    return glfw.glfwGetWindowMonitor(self.handle);
end

--TODO: Implement `Monitor`
--[=[
    Sets the mode, monitor, video mode and placement of a window.
]=]
function window.setMonitor(self: Window, monitor: FFIPointer, xpos: number, ypos: number, width: number, height: number, refreshRate: number): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSetWindowMonitor(self.handle, monitor, xpos, ypos, width, height, refreshRate);
end

--[=[
    Makes the context of the specified window current for the calling thread.
]=]
function window.makeContextCurrent(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwMakeContextCurrent(self.handle);
end

--[=[
    Sets the position callback for the specified window.
]=]
function window.setPositionCallback(self: Window, callback: (window: Window, xpos: number, ypos: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    local closure = ffi.closure(glfw.GLFWwindowposfun, callback);
    self.callbacks.pos = closure;
    glfw.glfwSetWindowPosCallback(self.handle, closure);
end

--[=[
    Sets the size callback for the specified window.
]=]
function window.setSizeCallback(self: Window, callback: (window: Window, width: number, height: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    local closure = ffi.closure(glfw.GLFWwindowsizefun, callback);
    self.callbacks.size = closure;
    glfw.glfwSetWindowSizeCallback(self.handle, closure);
end

local function setOptionalCallback(name: string, ffiFn : (...any) -> FFIPointer, window: Window, fnDef: FFIFunctionDefintion, callback: ((...any) -> any)?): ()
    local previousCallback = window.callbacks[name];
    if (previousCallback) then
        previousCallback:release();
    end
    if (not callback) then
        ffiFn(window.handle, nil);
        window.callbacks[name] = nil;
        return;
    end
    local closure = ffi.closure(glfw.GLFWwindowclosefun, function(...)
        local args = {...};
        if (args[1] == window.handle) then
            return callback(window, select(2, ...));
        end
        return callback(...);
    end);
    ffiFn(window.handle, closure);
    window.callbacks[name] = closure;
end

--[=[
    Sets the close callback for the specified window.
]=]
function window.setCloseCallback(self: Window, callback: ((window: Window) -> ())?): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "close",
        glfw.glfwSetWindowCloseCallback,
        self,
        glfw.GLFWwindowclosefun,
        callback
    );
end

--[=[
    Sets the refresh callback for the specified window.
]=]
function window.setRefreshCallback(self: Window, callback: (window: Window) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "refresh",
        glfw.glfwSetWindowRefreshCallback,
        self,
        glfw.GLFWwindowrefreshfun,
        callback
    );
end

--[=[
    Sets the focus callback for the specified window.
]=]
function window.setFocusCallback(self: Window, callback: (window: Window, focused: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "focus",
        glfw.glfwSetWindowFocusCallback,
        self,
        glfw.GLFWwindowfocusfun,
        callback
    );
end

--[=[
    Sets the iconify callback for the specified window.
]=]
function window.setIconifyCallback(self: Window, callback: (window: Window, iconified: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "iconify",
        glfw.glfwSetWindowIconifyCallback,
        self,
        glfw.GLFWwindowiconifyfun,
        callback
    );
end

--[=[
    Sets the maximize callback for the specified window.
]=]
function window.setMaximizeCallback(self: Window, callback: (window: Window, maximized: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "maximize",
        glfw.glfwSetWindowMaximizeCallback,
        self,
        glfw.GLFWwindowmaximizefun,
        callback
    );
end

--[=[
    Sets the framebuffer size callback for the specified window.
]=]
function window.setFramebufferSizeCallback(self: Window, callback: (window: Window, width: number, height: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "framebufferSize",
        glfw.glfwSetFramebufferSizeCallback,
        self,
        glfw.GLFWframebuffersizefun,
        callback
    );
end

--[=[
    Sets the window content scale callback for the specified window.
]=]
function window.setContentScaleCallback(self: Window, callback: (window: Window, xScale: number, yScale: number) -> ()): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    setOptionalCallback(
        "contentScale",
        glfw.glfwSetWindowContentScaleCallback,
        self,
        glfw.GLFWwindowcontentscalefun,
        callback
    );
end

--[=[
    Swaps the front and back buffers of the window.
]=]
function window.swapBuffers(self: Window): ()
    if (not self.handle) then
        error("Window handle is invalid");
    end
    glfw.glfwSwapBuffers(self.handle);
end

--[=[
    Returns whether the window should close.
]=]
function window.shouldClose(self: Window): boolean
    if (not self.handle) then
        return true;
    end
    return glfw.glfwWindowShouldClose(self.handle) == glfw.GLFW_TRUE;
end

--[=[
    Destroys the specified window and its context.
]=]
function window.destroy(self: Window): ()
    if (not self.handle) then
        return;
    end
    glfw.glfwDestroyWindow(self.handle);
    self.handle = nil::any;
end

export type Window = typeof(window.init(ffi.alloc(0)));

return window;


local Utils = require('./utils')

-- converts a string to kebab case ("kebab-case")
local function toKebabCase(str: string): string
    return Utils.transform(str, Utils.lowercase, '-')
end

return {
    toKebabCase = toKebabCase,
}


local Utils = require('./utils')

-- converts a string to lower camel case ("lowerCamelCase")
local function toLowerCamelCase(str: string): string
    local first = true
    return Utils.transform(str, function(chars, start, end_, formatter)
        if first then
            first = false
            Utils.lowercase(chars, start, end_, formatter)
        else
            Utils.capitalize(chars, start, end_, formatter)
        end
    end, nil)
end

return {
    toLowerCamelCase = toLowerCamelCase,
}


local Utils = require('./utils')

-- converts a string to shouty kebab case ("SHOUTY-KEBAB-CASE")
local function toShoutyKebabCase(str: string): string
    return Utils.transform(str, Utils.uppercase, '-')
end

return {
    toShoutyKebabCase = toShoutyKebabCase,
}


local Utils = require('./utils')

-- converts a string to shouty snake case ("SHOUTY_SNAKE_CASE")
local function toShoutySnakeCase(str: string): string
    return Utils.transform(str, Utils.uppercase, '_')
end

return {
    toShoutySnakeCase = toShoutySnakeCase,
}


local Utils = require('./utils')

-- converts a string to snake case ("snake_case_example")
local function toSnakeCase(str: string): string
    return Utils.transform(str, Utils.lowercase, '_')
end

return {
    toSnakeCase = toSnakeCase,
}


local Utils = require('./utils')

-- converts a string to title case ("Title Case")
local function toTitleCase(str: string): string
    return Utils.transform(str, Utils.capitalize, ' ')
end

return {
    toTitleCase = toTitleCase,
}


local Utils = require('./utils')

-- converts a string to train case ("Train-Case")
local function toTrainCase(str: string): string
    return Utils.transform(str, Utils.capitalize, '-')
end

return {
    toTrainCase = toTrainCase,
}


local Utils = require('./utils')

-- converts a string to upper camel case ("UpperCamelCase")
local function toUpperCamelCase(str: string): string
    return Utils.transform(str, Utils.capitalize, '')
end

return {
    toUpperCamelCase = toUpperCamelCase,
}


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Heck = require('..')

local expect = jestGlobals.expect
local it = jestGlobals.it
local describe = jestGlobals.describe

local testString = 'camelCase'

describe('toSnakeCase', function()
    it('should convert to snake_case', function()
        expect(Heck.toSnakeCase(testString)).toBe('camel_case')
    end)
end)

describe('toUpperCamelCase', function()
    it('should convert to UpperCamelCase', function()
        expect(Heck.toUpperCamelCase('snake_case')).toBe('SnakeCase')
    end)
end)

describe('toLowerCamelCase', function()
    it('should convert to lowerCamelCase', function()
        expect(Heck.toLowerCamelCase('snake_case')).toBe('snakeCase')
    end)
end)

describe('toKebabCase', function()
    it('should convert to kebab-case', function()
        expect(Heck.toKebabCase(testString)).toBe('camel-case')
    end)
end)

describe('toShoutySnakeCase', function()
    it('should convert to SHOUTY_SNAKE_CASE', function()
        expect(Heck.toShoutySnakeCase(testString)).toBe('CAMEL_CASE')
    end)
end)

describe('toShoutyKebabCase', function()
    it('should convert to SHOUTY-KEBAB-CASE', function()
        expect(Heck.toShoutyKebabCase(testString)).toBe('CAMEL-CASE')
    end)
end)

describe('toTitleCase', function()
    it('should convert to Title Case', function()
        expect(Heck.toTitleCase(testString)).toBe('Camel Case')
    end)
end)

describe('toTrainCase', function()
    it('should convert to Train-Case', function()
        expect(Heck.toTrainCase(testString)).toBe('Camel-Case')
    end)
end)


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Kebab = require('../Kebab')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(Kebab.toKebabCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'camel-case')
t('test2', 'This is Human case.', 'this-is-human-case')
t('test3', 'MixedUP CamelCase, with some Spaces', 'mixed-up-camel-case-with-some-spaces')
t('test4', 'mixed_up_ snake_case with some _spaces', 'mixed-up-snake-case-with-some-spaces')
t('test5', 'kebab-case', 'kebab-case')
t('test6', 'SHOUTY_SNAKE_CASE', 'shouty-snake-case')
t('test7', 'snake_case', 'snake-case')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'this-contains-all-kinds-of-word-boundaries'
)
t('test9', 'XΣXΣ baﬄe', 'xσxς-baﬄe')
t('test10', 'XMLHttpRequest', 'xml-http-request')
t(
    'test11',
    'لِنَذْهَبْ إِلَى السِّيْنَمَا',
    'لِنَذْهَبْ-إِلَى-السِّيْنَمَا'
)
-- Japanese and Chinese do not have word separation.
t('test12', 'ファイルを読み込み', 'ファイルを読み込み')
t('test13', '祝你一天过得愉快', '祝你一天过得愉快')


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local ShoutyKebab = require('../ShoutyKebab')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(ShoutyKebab.toShoutyKebabCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'CAMEL-CASE')
t('test2', 'This is Human case.', 'THIS-IS-HUMAN-CASE')
t('test3', 'MixedUP CamelCase, with some Spaces', 'MIXED-UP-CAMEL-CASE-WITH-SOME-SPACES')
t('test4', 'mixed_up_ snake_case with some _spaces', 'MIXED-UP-SNAKE-CASE-WITH-SOME-SPACES')
t('test5', 'kebab-case', 'KEBAB-CASE')
t('test6', 'SHOUTY_SNAKE_CASE', 'SHOUTY-SNAKE-CASE')
t('test7', 'snake_case', 'SNAKE-CASE')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'THIS-CONTAINS-ALL-KINDS-OF-WORD-BOUNDARIES'
)
t('test9', 'XΣXΣ baﬄe', 'XΣXΣ-BAFFLE')
t('test10', 'XMLHttpRequest', 'XML-HTTP-REQUEST')
t('test11', 'SHOUTY-KEBAB-CASE', 'SHOUTY-KEBAB-CASE')


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local ShoutySnake = require('../ShoutySnake')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(ShoutySnake.toShoutySnakeCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'CAMEL_CASE')
t('test2', 'This is Human case.', 'THIS_IS_HUMAN_CASE')
t('test3', 'MixedUP CamelCase, with some Spaces', 'MIXED_UP_CAMEL_CASE_WITH_SOME_SPACES')
t('test4', 'mixed_up_snake_case with some _spaces', 'MIXED_UP_SNAKE_CASE_WITH_SOME_SPACES')
t('test5', 'kebab-case', 'KEBAB_CASE')
t('test6', 'SHOUTY_SNAKE_CASE', 'SHOUTY_SNAKE_CASE')
t('test7', 'snake_case', 'SNAKE_CASE')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'THIS_CONTAINS_ALL_KINDS_OF_WORD_BOUNDARIES'
)
t('test9', 'XΣXΣ baﬄe', 'XΣXΣ_BAFFLE')
t('test10', 'XMLHttpRequest', 'XML_HTTP_REQUEST')


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Snake = require('../Snake')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(Snake.toSnakeCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'camel_case')
t('test2', 'This is Human case.', 'this_is_human_case')
t('test3', 'MixedUP CamelCase, with some Spaces', 'mixed_up_camel_case_with_some_spaces')
t('test4', 'mixed_up_ snake_case with some _spaces', 'mixed_up_snake_case_with_some_spaces')
t('test5', 'kebab-case', 'kebab_case')
t('test6', 'SHOUTY_SNAKE_CASE', 'shouty_snake_case')
t('test7', 'snake_case', 'snake_case')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'this_contains_all_kinds_of_word_boundaries'
)
t('test9', 'XΣXΣ baﬄe', 'xσxς_baﬄe')
t('test10', 'XMLHttpRequest', 'xml_http_request')
t('test11', 'FIELD_NAME11', 'field_name11')
t('test12', '99BOTTLES', '99bottles')
t('test13', 'FieldNamE11', 'field_nam_e11')
t('test14', 'abc123def456', 'abc123def456')
t('test16', 'abc123DEF456', 'abc123_def456')
t('test17', 'abc123Def456', 'abc123_def456')
t('test18', 'abc123DEf456', 'abc123_d_ef456')
t('test19', 'ABC123def456', 'abc123def456')
t('test20', 'ABC123DEF456', 'abc123def456')
t('test21', 'ABC123Def456', 'abc123_def456')
t('test22', 'ABC123DEf456', 'abc123d_ef456')
t('test23', 'ABC123dEEf456FOO', 'abc123d_e_ef456_foo')
t('test24', 'abcDEF', 'abc_def')
t('test25', 'ABcDE', 'a_bc_de')


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Title = require('../Title')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(Title.toTitleCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'Camel Case')
t('test2', 'This is Human case.', 'This Is Human Case')
t('test3', 'MixedUP CamelCase, with some Spaces', 'Mixed Up Camel Case With Some Spaces')
t('test4', 'mixed_up_ snake_case, with some _spaces', 'Mixed Up Snake Case With Some Spaces')
t('test5', 'kebab-case', 'Kebab Case')
t('test6', 'SHOUTY_SNAKE_CASE', 'Shouty Snake Case')
t('test7', 'snake_case', 'Snake Case')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'This Contains All Kinds Of Word Boundaries'
)
t('test9', 'XΣXΣ baﬄe', 'Xσxς Baﬄe')
t('test10', 'XMLHttpRequest', 'Xml Http Request')


local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local Train = require('../Train')

local expect = jestGlobals.expect
local it = jestGlobals.it

local function t(testName: string, input: string, expected: string)
    it(`'{input}' converts to '{expected}' ({testName})`, function()
        expect(Train.toTrainCase(input)).toBe(expected)
    end)
end

t('test1', 'CamelCase', 'Camel-Case')
t('test2', 'This is Human case.', 'This-Is-Human-Case')
t('test3', 'MixedUP CamelCase, with some Spaces', 'Mixed-Up-Camel-Case-With-Some-Spaces')
t('test4', 'mixed_up_ snake_case with some _spaces', 'Mixed-Up-Snake-Case-With-Some-Spaces')
t('test5', 'kebab-case', 'Kebab-Case')
t('test6', 'SHOUTY_SNAKE_CASE', 'Shouty-Snake-Case')
t('test7', 'snake_case', 'Snake-Case')
t(
    'test8',
    'this-contains_ ALLKinds OfWord_Boundaries',
    'This-Contains-All-Kinds-Of-Word-Boundaries'
)
-- #[cfg(feature = "unicode")]
t('test9', 'XΣXΣ baﬄe', 'Xσxς-Baﬄe')
t('test10', 'XMLHttpRequest', 'Xml-Http-Request')
t('test11', 'FIELD_NAME11', 'Field-Name11')
t('test12', '99BOTTLES', '99bottles')
t('test13', 'FieldNamE11', 'Field-Nam-E11')
t('test14', 'abc123def456', 'Abc123def456')
t('test16', 'abc123DEF456', 'Abc123-Def456')
t('test17', 'abc123Def456', 'Abc123-Def456')
t('test18', 'abc123DEf456', 'Abc123-D-Ef456')
t('test19', 'ABC123def456', 'Abc123def456')
t('test20', 'ABC123DEF456', 'Abc123def456')
t('test21', 'ABC123Def456', 'Abc123-Def456')
t('test22', 'ABC123DEf456', 'Abc123d-Ef456')
t('test23', 'ABC123dEEf456FOO', 'Abc123d-E-Ef456-Foo')
t('test24', 'abcDEF', 'Abc-Def')
t('test25', 'ABcDE', 'A-Bc-De')


local Heck = require('../init')
local jestGlobals = require('@pkg/@jsdotlua/jest-globals')

local expect = jestGlobals.expect
local it = jestGlobals.it
local describe = jestGlobals.describe

describe('Edge Cases and Unicode Handling', function()
    describe('Empty and Nil Inputs', function()
        it('should handle empty string', function()
            expect(Heck.toSnakeCase('')).toBe('')
            expect(Heck.toUpperCamelCase('')).toBe('')
            expect(Heck.toLowerCamelCase('')).toBe('')
            expect(Heck.toKebabCase('')).toBe('')
            expect(Heck.toShoutySnakeCase('')).toBe('')
            expect(Heck.toShoutyKebabCase('')).toBe('')
            expect(Heck.toTitleCase('')).toBe('')
            expect(Heck.toTrainCase('')).toBe('')
        end)

        it('should handle nil input', function()
            local nilString: string = nil :: any
            expect(Heck.toSnakeCase(nilString)).toBe('')
            expect(Heck.toUpperCamelCase(nilString)).toBe('')
            expect(Heck.toLowerCamelCase(nilString)).toBe('')
            expect(Heck.toKebabCase(nilString)).toBe('')
            expect(Heck.toShoutySnakeCase(nilString)).toBe('')
            expect(Heck.toShoutyKebabCase(nilString)).toBe('')
            expect(Heck.toTitleCase(nilString)).toBe('')
            expect(Heck.toTrainCase(nilString)).toBe('')
        end)
    end)

    describe('Special Characters', function()
        it('should handle strings with numbers', function()
            expect(Heck.toSnakeCase('camelCase123')).toBe('camel_case123')
            expect(Heck.toUpperCamelCase('snake_case_123')).toBe('SnakeCase123')
            expect(Heck.toLowerCamelCase('snake_case_123')).toBe('snakeCase123')
        end)

        it('should handle strings with special characters', function()
            expect(Heck.toSnakeCase('camelCase!@#')).toBe('camel_case')
            expect(Heck.toUpperCamelCase('snake_case_!@#')).toBe('SnakeCase')
            expect(Heck.toLowerCamelCase('snake_case_!@#')).toBe('snakeCase')
        end)
    end)

    describe('Unicode Support', function()
        it('should handle Unicode characters', function()
            expect(Heck.toSnakeCase('camelCase你好')).toBe('camel_case你好')
            expect(Heck.toUpperCamelCase('snake_case_你好')).toBe('SnakeCase你好')
            expect(Heck.toLowerCamelCase('snake_case_你好')).toBe('snakeCase你好')
        end)

        it('should handle mixed Unicode and ASCII', function()
            expect(Heck.toSnakeCase('camelCase你好World')).toBe('camel_case你好_world')
            expect(Heck.toUpperCamelCase('snake_case_你好_world')).toBe('SnakeCase你好World')
            expect(Heck.toLowerCamelCase('snake_case_你好_world')).toBe('snakeCase你好World')
        end)
    end)

    describe('Multiple Word Boundaries', function()
        it('should handle multiple consecutive word boundaries', function()
            expect(Heck.toSnakeCase('camelCase__with__multiple__boundaries')).toBe(
                'camel_case_with_multiple_boundaries'
            )
            expect(Heck.toKebabCase('camelCase--with--multiple--boundaries')).toBe(
                'camel-case-with-multiple-boundaries'
            )
        end)

        it('should handle mixed word boundaries', function()
            expect(Heck.toSnakeCase('camelCase-with_mixed-boundaries')).toBe(
                'camel_case_with_mixed_boundaries'
            )
            expect(Heck.toKebabCase('camelCase-with_mixed-boundaries')).toBe(
                'camel-case-with-mixed-boundaries'
            )
        end)
    end)

    describe('Single Character Words', function()
        it('should handle single character words', function()
            expect(Heck.toSnakeCase('a_b_c')).toBe('a_b_c')
            expect(Heck.toUpperCamelCase('a_b_c')).toBe('ABC')
            expect(Heck.toLowerCamelCase('a_b_c')).toBe('aBC')
        end)
    end)
end)


local Kebab = require('./Kebab')
local LowerCamel = require('./LowerCamel')
local ShoutyKebab = require('./ShoutyKebab')
local ShoutySnake = require('./ShoutySnake')
local Snake = require('./Snake')
local Title = require('./Title')
local Train = require('./Train')
local UpperCamel = require('./UpperCamel')

return {
    toSnakeCase = Snake.toSnakeCase,
    toUpperCamelCase = UpperCamel.toUpperCamelCase,
    toPascalCase = UpperCamel.toUpperCamelCase,
    toLowerCamelCase = LowerCamel.toLowerCamelCase,
    toKebabCase = Kebab.toKebabCase,
    toShoutySnakeCase = ShoutySnake.toShoutySnakeCase,
    toShoutyKebabCase = ShoutyKebab.toShoutyKebabCase,
    toTitleCase = Title.toTitleCase,
    toTrainCase = Train.toTrainCase,
}


local function splitString(str: string, condition: (string) -> boolean): { string }
    local result = {}
    local currentWord = 0

    for position, c in utf8.codes(str) do
        local character = utf8.char(c)

        if condition(character) then
            table.insert(result, string.sub(str, currentWord + 1, position - 1))
            currentWord = position + #character - 1
        end
    end

    local length = #str

    if length == 0 then
        return { '' }
    elseif currentWord <= length then
        table.insert(result, string.sub(str, currentWord + 1))
    end

    return result
end

return splitString


local character = require('@pkg/luau-character')

local splitString = require('./rust-utils/splitString')

local isAlphaNumeric = character.isAlphaNumeric
local isLowercase = character.isLowercase
local isUppercase = character.isUppercase
local toLowercase = character.toLowercase
local toUppercase = character.toUppercase

local function getCodepoints(str: string): { string }
    local codepoints = {}
    for _, c in utf8.codes(str) do
        table.insert(codepoints, utf8.char(c))
    end
    return codepoints
end

local function isNotAlphaNumeric(char: string): boolean
    return not isAlphaNumeric(char)
end

--[[
    Word mode enum for tracking the case of the last cased character.
]]
local WordMode = {
    -- No lowercase or uppercase characters in current word
    Boundary = 0,
    -- Previous cased character was lowercase
    Lowercase = 1,
    -- Previous cased character was uppercase
    Uppercase = 2,
}

local function transform(
    str: string,
    withWord: (
        chars: { string },
        start: number,
        end_: number,
        formatter: { string }
    ) -> (),
    boundary: string?
): string
    if type(str) ~= 'string' or str == '' then
        return ''
    end

    local words = splitString(str, isNotAlphaNumeric)
    local firstWord = true

    local formatter = {}

    for _, word in words do
        local init = 1
        local mode = WordMode.Boundary
        local wordChars = getCodepoints(word)

        for i, c in wordChars do
            local nextI = i + 1
            local nextChar = wordChars[nextI]

            if nextChar then
                -- The mode including the current character, assuming the
                -- current character does not result in a word boundary.
                local nextMode = if isLowercase(c)
                    then WordMode.Lowercase
                    elseif isUppercase(c) then WordMode.Uppercase
                    else mode

                -- Word boundary after if current is not uppercase and next
                -- is uppercase
                if nextMode == WordMode.Lowercase and isUppercase(nextChar) then
                    if not firstWord then
                        if boundary ~= nil then
                            table.insert(formatter, boundary)
                        end
                    end
                    withWord(wordChars, init, i, formatter)
                    firstWord = false
                    init = nextI
                    mode = WordMode.Boundary

                -- Otherwise if current and previous are uppercase and next
                -- is lowercase, word boundary before
                elseif mode == WordMode.Uppercase and isUppercase(c) and isLowercase(nextChar) then
                    if not firstWord then
                        if boundary ~= nil then
                            table.insert(formatter, boundary)
                        end
                    else
                        firstWord = false
                    end
                    withWord(wordChars, init, i - 1, formatter)
                    init = i
                    mode = WordMode.Boundary
                else
                    -- Otherwise no word boundary, just update the mode
                    mode = nextMode
                end
            else
                -- Collect trailing characters as a word
                if not firstWord then
                    if boundary ~= nil then
                        table.insert(formatter, boundary)
                    end
                else
                    firstWord = false
                end
                withWord(wordChars, init, i, formatter)
                break
            end
        end
    end

    return table.concat(formatter, '')
end

--[[
    Converts a string to lowercase.
    @param str string The input string
    @return string The lowercase string
]]
local function lowercase(chars: { string }, start: number, end_: number, formatter: { string })
    for i = start, end_ do
        local c = chars[i]
        if c == 'Σ' and chars[i + 1] == nil then
            table.insert(formatter, 'ς')
        else
            table.insert(formatter, toLowercase(c))
        end
    end
end

--[[
    Converts a string to uppercase.
    @param str string The input string
    @return string The uppercase string
]]
local function uppercase(chars: { string }, start: number, end_: number, formatter: { string })
    for i = start, end_ do
        local c = chars[i]
        table.insert(formatter, toUppercase(c))
    end
end

--[[
    Capitalizes the first character of a string.
    @param str string The input string
    @return string The capitalized string
]]
local function capitalize(chars: { string }, start: number, end_: number, formatter: { string })
    local first = chars[start]
    if first ~= nil then
        table.insert(formatter, toUppercase(first))
        if start < end_ then
            lowercase(chars, start + 1, end_, formatter)
        end
    end
end

return {
    transform = transform,
    lowercase = lowercase,
    uppercase = uppercase,
    capitalize = capitalize,
}


--!strict
local scoped = require("./scoped.luau")
local scoped, doCleanup = scoped.scoped, scoped.doCleanup

local constructors = {}

function constructors.Person(
	cleanup: scoped.CleanupTable,
	name: string
)
	local self = {
		name = name
	}
	table.insert(cleanup, function()
		print(name, "has left the building!")
	end)
	return self
end

local scope = scoped(constructors)

local sally = scope:Person("Sally")
local bob = scope:Person("Bob")
local mortimer = scope:Person("Mortimer")
local geoff = scope:Person("Geoff")

doCleanup(scope)


local lib = require("../src/lib")

lib.build(
	"pesde.toml",
	{
		luau = true,
		lune = true,
		roblox = true,
	},
	"dist",
	{
	"src",
	},
	"LUA_ENV",
	"pkg"
)


local argparse = require("../lune_packages/argparse")
local multitarget = require("./lib")
local pathfs = require("../lune_packages/pathfs")
local fs = pathfs.fs
local stdio = require("@lune/stdio")
local serde = require("@lune/serde")
local process = require("@lune/process")
local chalk = require("../luau_packages/chalk_luau")
local tableHelper = require("../luau_packages/table_helper")

local DEFAULT_OUTPUT_DIR = multitarget.DEFAULT_OUTPUT_DIR
local DEFAULT_LUA_ENV_GLOBAL = multitarget.DEFAULT_LUA_ENV_GLOBAL
local DEFAULT_PKG_ALIAS = multitarget.DEFAULT_PKG_ALIAS
local ERROR_PREFIX = `{stdio.color("red")}{stdio.style("bold")}error{stdio.color("reset")}:`
local WARN_PREFIX = `{stdio.color("yellow")}{stdio.style("bold")}warn{stdio.color("reset")}:`
local DEFAULT_DECLARATION_FILE_CONTENT = `declare _G: \{\
	{DEFAULT_LUA_ENV_GLOBAL}: ("luau" | "lune" | "roblox")?,\
\}\
`
local COLORFUL_TARGETS = {
	luau = chalk.bgBlue(chalk.bold("Luau")),
	lune = chalk.bgMagenta(chalk.bold("Lune")),
	roblox = chalk.red(chalk.bold("ROBLOX")),
}
local DEFAULT_LUAU_PKG_ALIAS = multitarget.DEFAULT_LUAU_PKG_ALIAS

local CLI_NAME = "multitarget"
local cli = argparse(CLI_NAME, "A tool to build a pesde project for multiple targets")

local build = cli:command("build", "Build a pesde project for multiple targets")
build:argument("project", "Path to the pesde.toml file"):default("pesde.toml")
build:option("-o --output", "Output directory"):default(DEFAULT_OUTPUT_DIR)
build:option("-g --global", "Lua environment global variable name"):default(DEFAULT_LUA_ENV_GLOBAL)
build:option("-p --pkg", "Package alias name"):default(DEFAULT_PKG_ALIAS)
build:option("--luau-pkg", "Luau Package alias name (Used for Lune target)"):default(DEFAULT_LUAU_PKG_ALIAS)
build:flag("--luau", "Build for Luau")
build:flag("--lune", "Build for Lune and Prefix for luau package alias")
build:flag("--roblox", "Build for Roblox")
build:flag("-a --all", "Build for every targets (luau, lune, roblox)")
build:option("--build-files", "Paths to build files"):args("*")
build:flag("-y --yes", "Automatically confirm prompts without asking")
build:flag("--non-strict", "Do not fail the build when there are errors while re-adding dependencies")
build:option("--require-mode", "Which darklua require mode the current source code is using"):default("luau")
build:action(function(args: {
	project: string,
	output: string,
	global: string,
	pkg: string,
	luau_pkg: string,
	luau: boolean,
	lune: boolean,
	roblox: boolean,
	all: boolean,
	build_files: { string }?,
	yes: boolean,
	non_strict: boolean,
	require_mode: string,
})
	local output = args.output
	if fs.isDir(output) then
		if args.yes or stdio.prompt("confirm", "Output directory already exists. Would you like to overwrite it?") then
			fs.removeDir(output)
		else
			print("Canceled. Output directory already exists.")
			process.exit(1)
		end
	end
	if args.all then
		args.luau = true
		args.lune = true
		args.roblox = true
	end
	local targets = {
		luau = args.luau,
		lune = args.lune,
		roblox = args.roblox,
	}
	local result = multitarget.build(
		args.project,
		targets,
		output,
		args.build_files,
		args.global,
		args.pkg,
		args.luau_pkg,
		not args.non_strict,
		args.require_mode
	)
	if result:isErr() then
		local err = result:unwrapErr()

		if err.outputDir then -- rollback!
			print(`{WARN_PREFIX} Error occurred while building the project. Deleting the output directory...`)
			err.outputDir:removeDir()
		end

		stdio.ewrite(`{ERROR_PREFIX} {err.message}\n`)
		process.exit(1)
		error("unreachable")
	end

	local colorfulTargets = {}
	for target: string, enabled: boolean in targets :: any do
		if enabled then
			table.insert(colorfulTargets, COLORFUL_TARGETS[target])
		end
	end
	print(
		`\n{stdio.color("green")}Build completed!{stdio.color("reset")} {table.concat(colorfulTargets, ", ")}\nTo publish them, run 'cd {output} && pesde publish'`
	)
end)

local function tableGet(t: {}, path: { any }): any?
	local current = t
	for i = 1, #path - 1 do
		local key = path[i]
		if not current[key] then
			return nil
		end
		current = current[key]
	end
	return current[path[#path]]
end

local setup = cli:command("setup", "Setup your project before building")
setup:action(function()
	do
		local pesdeToml = pathfs.File.new("pesde.toml")
		if not pesdeToml:isFile() then
			stdio.ewrite(`{ERROR_PREFIX} pesde.toml is not found. Are you in the right directory?\n`)
			process.exit(1)
			error("unreachable")
		end
		local pesdeManifest: {
			private: boolean?,
			dependencies: { [string]: any }?,
			peer_dependencies: { [string]: any }?,
			target: {
				environment: ("luau" | "lune" | "roblox")?,
			}?,
		} =
			serde.decode("toml", pesdeToml:readFile())
		if not pesdeManifest.private then
			print(
				`{WARN_PREFIX} It is recommended to set 'private = true' in pesde.toml to prevent accidental publishing the root project.`
			)
		end
		local luaurcFile = pathfs.File.new(".luaurc")
		local luaurc: {
			aliases: { [string]: string? }?,
		} = if luaurcFile:isFile()
			then serde.decode("json", luaurcFile:readFile())
			else {}
		if not pesdeManifest.target or not pesdeManifest.target.environment then
			stdio.ewrite(`{ERROR_PREFIX} 'target.environment' is not found in pesde.toml\n`)
			process.exit(1)
			error("unreachable")
		end
		local pkgPath = `./{pesdeManifest.target.environment}_packages/`
		local pkgAlias = luaurc.aliases and luaurc.aliases[DEFAULT_PKG_ALIAS]
		local hasDependencies = (
			(pesdeManifest.dependencies and next(pesdeManifest.dependencies))
			or (pesdeManifest.peer_dependencies and next(pesdeManifest.peer_dependencies))
		)
		local isPkgAliasInvalid = if pkgAlias then pkgAlias ~= pkgPath else false
		if (not pkgAlias and hasDependencies) or isPkgAliasInvalid then
			if isPkgAliasInvalid then
				print(
					`{WARN_PREFIX} @{DEFAULT_PKG_ALIAS} alias already used. This will be replaced with './\{target\}_packages/' for each target's dependencies when you build the project.`
				)
			end
			if
				stdio.prompt(
					"confirm",
					`Found dependencies in pesde.toml. Would you like to add default aliases '@{DEFAULT_PKG_ALIAS}' and '@{DEFAULT_LUAU_PKG_ALIAS}' in .luaurc for each target's dependencies?`
				)
			then
				tableHelper.set(luaurc, { "aliases", DEFAULT_PKG_ALIAS }, pkgPath)
				tableHelper.set(luaurc, { "aliases", DEFAULT_LUAU_PKG_ALIAS }, "./luau_packages/")
				luaurcFile:writeFile(serde.encode("json", luaurc, true))
			end
		end
	end
	do
		local gitignore = pathfs.findFile(".gitignore")
		if gitignore then
			local toIgnore = `/{DEFAULT_OUTPUT_DIR}`
			local content = gitignore:readFile()
			local found = false
			for _, line in content:split("\n") do
				if line == toIgnore then
					found = true
					break
				end
			end
			if not found then
				if
					stdio.prompt(
						"confirm",
						`Found a .gitignore file. Would you like to add the default output directory('{toIgnore}') to it?`
					)
				then
					content ..= "\n" .. toIgnore .. "\n"
					gitignore:writeFile(content)
				end
			end
		end
	end
	do
		local seleneFile = pathfs.findFile("selene.toml")
		if seleneFile then
			-- selene: allow(shadowing)
			local seleneToml: {
				config: {
					global_usage: {
						ignore_pattern: string?,
					}?,
				}?,
			} =
				serde.decode("toml", seleneFile:readFile())
			local path = { "config", "global_usage", "ignore_pattern" }
			if not tableGet(seleneToml, path) then
				if
					stdio.prompt(
						"confirm",
						`Found a selene.toml file. Would you like to add the default lua environment global variable name('{DEFAULT_LUA_ENV_GLOBAL}') to the ignore pattern?`
					)
				then
					tableHelper.set(seleneToml, path, DEFAULT_LUA_ENV_GLOBAL)
					seleneFile:writeFile(serde.encode("toml", seleneToml, true))
				end
			end
		end
	end
	do
		local ok, result = pcall(process.exec, "darklua", { "--version" })
		if not ok or (ok and not result.ok) then
			if stdio.prompt("confirm", `darklua is not found. Would you like to install it from pesde?`) then
				process.exec("pesde", { "add", "pesde/darklua", "--dev", "-t", "lune" }, { stdio = "forward" })
				process.exec("pesde", { "install" }, { stdio = "forward" })
			end
		end
	end
	do
		local declarationFile = pathfs.File.new(".d.luau")
		if not declarationFile:isFile() then
			if
				stdio.prompt(
					"confirm",
					`Would you like to add .d.luau type definition file for default lua environment global variable name('{DEFAULT_LUA_ENV_GLOBAL}')?`
				)
			then
				declarationFile:writeFile(DEFAULT_DECLARATION_FILE_CONTENT)
			end
		end
	end
	do
		local vscodeDir = pathfs.Directory.new(".vscode")
		local vscodeSettings = pathfs.File.new(vscodeDir.path:join("settings.json"))
		local settings: {
			["luau-lsp.types.definitionFiles"]: { string }?,
		} = if vscodeSettings:isFile()
			then serde.decode("json", vscodeSettings:readFile())
			else {}
		if
			not settings["luau-lsp.types.definitionFiles"]
			or not table.find(settings["luau-lsp.types.definitionFiles"], ".d.luau")
		then
			if
				stdio.prompt(
					"confirm",
					"Would you like to add .d.luau type definition file to '.vscode/settings.json'?"
				)
			then
				local defFiles = settings["luau-lsp.types.definitionFiles"] or {}
				table.insert(defFiles, ".d.luau")
				settings["luau-lsp.types.definitionFiles"] = defFiles
				if not vscodeDir:isDir() then
					vscodeDir:writeDir()
				end
				vscodeSettings:writeFile(serde.encode("json", settings, true))
			end
		end
	end

	print(
		chalk.green(
			chalk.bold("You are ready to go! Try running 'multitarget build [arguments]' to build your project.")
		)
	)
end)

local version = cli:command("version", "Show the version of multitarget")
version:action(function()
	local multitargetPesdeToml = pathfs.File.fromExisting(pathfs.Path.from(_G.PESDE_ROOT):join("pesde.toml"))
	local pesdeManifest: {
		version: string,
	} = serde.decode("toml", multitargetPesdeToml:readFile())
	print(`{CLI_NAME} {pesdeManifest.version}`)
end)

cli:parse()


local stdio = require("@lune/stdio")
local serde = require("@lune/serde")
local pathfs = require("../lune_packages/pathfs")
local fs = pathfs.fs
local gt = require("../lune_packages/greentea")
local glob = require("../lune_packages/glob")
local process = require("@lune/process")
local darklua = require("../lune_packages/darklua_lune")
local Result = require("../lune_packages/result")
local tableHelper = require("../luau_packages/table_helper")

local DEFAULT_DARKLUA_RULES = {
	"remove_unused_if_branch",
	"filter_after_early_return",
	"remove_empty_do",
}
local DEFAULT_PESDE_TOML_DIR = "pesde.toml"
local DEFAULT_OUTPUT_DIR = "target"
local DEFAULT_LUA_ENV_GLOBAL = "LUA_ENV"
local DEFAULT_PKG_ALIAS = "pkg"
local DEFAULT_LUAU_PKG_ALIAS = "luau_pkg"

type Dependency = {
	name: string?,
	version: string?,
	wally: string?,
	workspace: string?,
	repo: string?,
	path: string?,
}

--[=[
	@class multitarget
	@prop DEFAULT_PESDE_TOML_DIR string
	@prop DEFAULT_OUTPUT_DIR string
	@prop DEFAULT_LUA_ENV_GLOBAL string
	@prop DEFAULT_PKG_ALIAS string

	# [pesde-multitarget](https://pesde.dev/packages/jiwonz/multitarget)
	Customizable and flexible build script for multiple pesde targets

	Build your pure pesde project easier targeting both `luau` and `roblox` targets at the same time.

	No more annoying package build scripts manually! (But this doesn't support `wally`)
]=]
local multitarget = {}

local targetsType = gt.build(gt.table({
	luau = gt.opt(gt.boolean()),
	lune = gt.opt(gt.boolean()),
	roblox = gt.opt(gt.boolean()),
}, {
	min = 1,
	max = 3,
}))
local environmentType = gt.build(gt.union(gt.literal("luau"), gt.literal("lune"), gt.literal("roblox")))
local optionalStringType = gt.build(gt.opt(gt.string()))
local optionalStringArrayType = gt.build(gt.opt(gt.array(gt.string())))
local optionalAsPathType = gt.build(gt.opt(pathfs.types.AsPath:type()))
local optionalBooleanType = gt.build(gt.opt(gt.boolean()))

local requireModeName = gt.union(gt.literal("path"), gt.literal("luau"))
local optionalRequireModeType = gt.build(gt.opt(gt.union(
	requireModeName,
	gt.table({
		name = requireModeName,
		sources = gt.opt(gt.dictionary(gt.string(), gt.string())),
		use_luau_configuration = gt.opt(gt.boolean()),
	})
)))

--[=[
	@within multitarget
	@param pesdeTomlPath string? -- Path to the `pesde.toml` file. Default is `pesde.toml`.
	@param targets { luau: boolean?, lune: boolean?, roblox: boolean? } -- Targets to build
	@param outputPath string? -- Output directory. Default is `dist`.
	@param buildFiles { string }? -- Paths to build files
	@param luaEnvGlobal string? -- Lua environment global variable name. Default is `LUA_ENV`.
	@param pkgAlias string? -- Package alias name. Default is `pkg`.
	@param strict boolean? -- Stop the process if any error occurs while re-adding dependencies. Defaults to `true`.
	@param requireMode (string | { name: string, sources: { [string]: string }?, use_luau_configuration: boolean? })? -- What require mode darklua should use when transforming code

	Builds a pesde project for multiple targets

	Pass `nil` to use default arguments.

	```lua
	local result = multitarget.build("pesde.toml", {
		luau = true,
		lune = true,
		roblox = true,
	}, "dist", { "build.lua" }, "LUA_ENV", "pkg")

	if result:isOk() then
		print("Built successfully!")
	else
		print("Error:", result:unwrapErr().message)
	end
	```
]=]
function multitarget.build(
	pesdeTomlPath: typeof(optionalAsPathType:type()),
	targets: typeof(targetsType:type()),
	outputPath: typeof(optionalAsPathType:type()),
	buildFiles: typeof(optionalStringArrayType:type()),
	luaEnvGlobal: typeof(optionalStringType:type()),
	pkgAlias: typeof(optionalStringType:type()),
	luauPkgAlias: typeof(optionalStringType:type()),
	strict: typeof(optionalBooleanType:type()),
	requireMode: typeof(optionalRequireModeType:type())
): Result.Result<pathfs.Directory, {
	message: string,
	outputDir: pathfs.Directory?,
}>
	pathfs.types.AsPath:assert(pesdeTomlPath)
	pathfs.types.AsPath:assert(outputPath)
	targetsType:assert(targets)
	optionalStringArrayType:assert(buildFiles)
	optionalStringType:assert(luaEnvGlobal)
	optionalStringType:assert(pkgAlias)
	optionalStringType:assert(luauPkgAlias)
	optionalBooleanType:assert(strict)
	optionalRequireModeType:assert(requireMode)

	-- selene: allow(shadowing)
	local luauPkgAlias = luauPkgAlias or DEFAULT_LUAU_PKG_ALIAS
	-- selene: allow(shadowing)
	local strict = strict == nil or strict
	-- selene: allow(shadowing)
	local pesdeTomlPath = pesdeTomlPath or DEFAULT_PESDE_TOML_DIR
	-- selene: allow(shadowing)
	local outputPath = outputPath or DEFAULT_OUTPUT_DIR
	-- selene: allow(shadowing)
	local luaEnvGlobal = luaEnvGlobal or DEFAULT_LUA_ENV_GLOBAL
	-- selene: allow(shadowing)
	local pkgAlias = pkgAlias or DEFAULT_PKG_ALIAS
	-- selene: allow(shadowing)
	local pesdeTomlFile = pathfs.File.fromExisting(pesdeTomlPath)
	local outputDir = pathfs.findDir(outputPath)
	if outputDir then
		return Result.Err({
			message = `Output directory '{outputDir.path}' already exists.`,
		})
	end
	-- selene: allow(shadowing)
	local outputDir = pathfs.Directory.create(outputPath)
	local workingDirPath = pathfs.canonicalize(pesdeTomlFile.path):parent() or pathfs.cwd()

	local pesdeToml: {
		includes: { string }?,
		target: {
			environment: typeof(environmentType:type())?,
			lib: string?,
			bin: string?,
			build_files: { string }?,
			scripts: { [string]: string }?,
		},
		dependencies: { [string]: Dependency }?,
		dev_dependencies: { [string]: Dependency }?,
		peer_dependencies: { [string]: Dependency }?,
		private: boolean?,
		engines: { [string]: string }?,
		wally_indices: { [string]: string }?,
	} =
		serde.decode("toml", pesdeTomlFile:readFile())

	if not pesdeToml.target then
		return Result.Err({
			message = `'target' field is required in 'pesde.toml'.`,
			outputDir = outputDir,
		})
	end
	-- selene: allow(shadowing)
	local buildFiles = (pesdeToml.target.build_files or buildFiles) :: typeof(buildFiles)

	local pesdeAddParams = {}
	local function getPackageName(dep: Dependency): string?
		if dep.name and dep.version then
			return `{dep.name}@{dep.version}`
		end
		if dep.wally and dep.version then
			return `wally#{dep.name}@{dep.version}`
		end
		return nil
	end
	if pesdeToml.dependencies then
		for alias, dep in pesdeToml.dependencies do
			local packageName = getPackageName(dep)
			if packageName then
				table.insert(pesdeAddParams, { "--alias", alias, packageName })
			end
		end
	end
	if pesdeToml.peer_dependencies then
		for alias, dep in pesdeToml.peer_dependencies do
			local packageName = getPackageName(dep)
			if packageName then
				table.insert(pesdeAddParams, { "--alias", alias, "--peer", packageName })
			end
		end
	end

	do
		local members = {}
		for t, value in targets :: { [typeof(environmentType:type())]: boolean? } do
			if value == nil then
				continue
			end
			table.insert(members, t)
		end
		local workspacePesdeToml = {
			name = "multitarget/root",
			version = "0.0.0",
			private = true,
			workspace_members = members,
			target = { environment = "luau" },
		}
		fs.writeFile(outputDir.path:join("pesde.toml"), serde.encode("toml", workspacePesdeToml, true))
	end

	if not buildFiles then
		if targets.roblox then
			return Result.Err({
				message = `Build files are required for 'roblox' target.`,
				outputDir = outputDir,
			})
		end
		print(
			`{stdio.color("yellow")}warn{stdio.color("reset")}: None of build files are specified. The files may not be processed properly. It is ok to skip if you haven't used '_G.{luaEnvGlobal}' in your build files.`
		)
	end

	for target: "lune" | "luau" | "roblox", value in targets :: { [any]: boolean? } do
		if not value then
			continue
		end
		environmentType:assert(target)

		local targetDir = pathfs.Directory.new(outputDir.path:join(target))
		targetDir:writeDir() -- assure it exists (may not exist later if the user uses "includes" incorrectly, which would throw an undescriptive error later when trying to access this directory)

		-- selene: allow(shadowing)
		local newPesdeToml = tableHelper.copy(pesdeToml, true)
		if newPesdeToml.includes then
			for _, include in newPesdeToml.includes do
				for _, path in
					glob(include, {
						cwd = workingDirPath,
					})
				do
					local dest = targetDir.path:join(
						-- Should be computed in normalized absolute path to avoid issues with relative paths
						pathfs.canonicalize(path):stripPrefix(workingDirPath)
							or error(
								`include glob "{include}" found file {path} which is not prefixed by {workingDirPath}`
							) -- this shouldn't really happen
					)
					local parent = dest:parent()
					if parent then
						fs.writeDir(parent)
					end
					fs.copy(path, dest, true)
				end
			end
		else
			if newPesdeToml.target.lib then
				local dest = targetDir.path:join(newPesdeToml.target.lib)
				local parent = dest:parent()
				if parent then
					fs.writeDir(parent)
				end
				fs.copy(workingDirPath:join(newPesdeToml.target.lib), dest, true)
			end
			if newPesdeToml.target.bin then
				local dest = targetDir.path:join(newPesdeToml.target.bin)
				local parent = dest:parent()
				if parent then
					fs.writeDir(parent)
				end
				fs.copy(workingDirPath:join(newPesdeToml.target.bin), dest, true)
			end
		end

		newPesdeToml.private = nil

		if #pesdeAddParams > 0 then -- there are dependencies
			local luaurcFile = pathfs.File.new(targetDir.path:join(".luaurc"))
			local luaurc: {
				aliases: { [string]: string? }?,
			} = if luaurcFile:isFile()
				then serde.decode("json", luaurcFile:readFile())
				else {}
			tableHelper.set(luaurc, { "aliases", pkgAlias }, `./{target}_packages/`)
			if targets.lune then
				if target == "lune" then
					tableHelper.set(luaurc, { "aliases", luauPkgAlias }, `./luau_packages/`)
				elseif target == "roblox" then
					tableHelper.set(luaurc, { "aliases", luauPkgAlias }, `./roblox_packages/`)
				end
			end
			luaurcFile:writeFile(serde.encode("json", luaurc, true))

			local includes = newPesdeToml.includes or {}
			if table.find(includes, ".luaurc") == nil then
				table.insert(includes, ".luaurc")
			end
			newPesdeToml.includes = includes
		end

		if target == "luau" or target == "lune" then
			newPesdeToml.target.build_files = nil
			newPesdeToml.wally_indices = nil
		elseif target == "roblox" then
			newPesdeToml.target.build_files = buildFiles
			newPesdeToml.target.bin = nil
			newPesdeToml.target.scripts = nil
			newPesdeToml.engines = nil
		end
		newPesdeToml.target.environment = target
		newPesdeToml.dev_dependencies = nil
		newPesdeToml.peer_dependencies = nil
		newPesdeToml.dependencies = nil
		fs.writeFile(targetDir.path:join("pesde.toml"), serde.encode("toml", newPesdeToml, true))

		for _, pesdeAddParam in pesdeAddParams do
			local params = { "add" }
			for _, param in pesdeAddParam do
				table.insert(params, param)
			end
			table.insert(params, "--target")
			table.insert(params, target)
			local result = process.exec("pesde", params, {
				cwd = targetDir.path:toString(),
				stdio = "inherit",
			})
			if not result.ok and target == "lune" then
				params[#params] = "luau"
				result = process.exec("pesde", params, {
					cwd = targetDir.path:toString(),
					stdio = "inherit",
				})
			end
			if strict and not result.ok then
				return Result.Err({
					message = `Failed to add dependency '{table.concat(pesdeAddParam, " ")}' to '{target}' target.`,
					outputDir = outputDir,
				})
			end
		end
	end

	local result = process.exec("pesde", { "install" }, {
		cwd = outputDir.path:toString(),
		stdio = "inherit",
	})

	if not result.ok then
		return Result.Err({
			message = `Failed to install dependencies.`,
			outputDir = outputDir,
		})
	end

	if buildFiles then
		for target: "lune" | "luau" | "roblox", value in targets :: { [any]: boolean? } do
			if not value then
				continue
			end

			local targetDir = pathfs.Directory.new(outputDir.path:join(target))
			local injectGlobalValueRule = {
				rule = "inject_global_value",
				identifier = luaEnvGlobal,
				value = target,
			}

			local rules = { injectGlobalValueRule :: any }
			for _, rule in DEFAULT_DARKLUA_RULES do
				table.insert(rules, rule)
			end

			if target == "roblox" then
				local currentRequireMode = if type(requireMode) == "table"
					then requireMode
					else {
						name = requireMode or "luau",
					}

				table.insert(rules, {
					rule = "convert_require",
					current = currentRequireMode,
					target = {
						name = "roblox",
						indexing_style = "find_first_child",
						rojo_sourcemap = if fs.isFile("sourcemap.json") then "sourcemap.json" else nil,
					},
				})
			end

			for _, buildFile in buildFiles do
				local darkluaOutputPath = targetDir.path:join(buildFile):toString()
				darklua.process(buildFile, darkluaOutputPath, {
					rules = rules,
				})
			end
		end
	end

	return Result.Ok(outputDir)
end

multitarget.DEFAULT_PESDE_TOML_DIR = DEFAULT_PESDE_TOML_DIR
multitarget.DEFAULT_OUTPUT_DIR = DEFAULT_OUTPUT_DIR
multitarget.DEFAULT_LUA_ENV_GLOBAL = DEFAULT_LUA_ENV_GLOBAL
multitarget.DEFAULT_PKG_ALIAS = DEFAULT_PKG_ALIAS
multitarget.DEFAULT_LUAU_PKG_ALIAS = DEFAULT_LUAU_PKG_ALIAS

return multitarget


-- ROBLOX no upstream

--[[
    ROBLOX note:
    custom implementation for BenchmarkJS
    only supporting necessary features
]]
local rootWorkspace = script.Parent.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
local Object = LuauPolyfill.Object
type Function = (...any) -> ...any

type Array<T> = LuauPolyfill.Array<T>
-- ROBLOX FIXME: fix in LuauPolyfill
type Object = { [string]: any }
type Promise<T> = LuauPolyfill.Promise<T>

local Promise = require(rootWorkspace.Promise)

local calculateStats = require(script.Parent.calculateStats)

type Stats = calculateStats.Stats

-- ROBLOX NOTE: maximum relative margin of error
local MAX_RME: number = 5
if _G.__MAX_RME__ then
	local parsedNum = tonumber(_G.__MAX_RME__)
	if typeof(parsedNum) == "number" and parsedNum == parsedNum then
		MAX_RME = parsedNum
	end
end

local function secToMs(sec: number): number
	return sec * 1000
end

local function printStats(benchmarkName: string, data: Array<number>)
	local stats = calculateStats(data)
	local hz = 1 / stats.mean
	print(("%s x %4.4f ops/sec ±%3.2f%% (%d runs sampled)"):format(benchmarkName, hz, stats.rme, stats.size))
end

local function notifyHandlers(target: any, eventName: string, event: Event)
	local ok, err_ = pcall(function()
		local options = target.options

		local eventHandlerProp = "on" .. string.upper(string.sub(eventName, 1, 1)) .. string.sub(eventName, 2)

		if typeof(options) == "table" and typeof(options[eventHandlerProp]) == "function" then
			local ok, err_ = pcall(options[eventHandlerProp], event)

			if not ok then
				warn(("error when calling %s handler: "):format(eventHandlerProp), err_)
			end
		end

		if typeof(target.handlers) == "table" and target.handlers[eventName] then
			for _, handler in ipairs(target.handlers[eventName]) do
				local ok, err_ = pcall(handler, event)

				if not ok then
					warn(("error when calling %s handler: "):format(eventName), err_)
				end
			end
		end
	end)

	if not ok then
		warn(err_)
	end
end

type Event = {
	type: string,
	target: any,
	timestamp: number,
} & Object

type BenchmarkOptions = Object

local Benchmark = {}

type Suite = {
	add: (self: Suite, name: string, options: BenchmarkOptions) -> Suite,
	on: (self: Suite, eventName: string, handler: (event: Event) -> ...any) -> Suite,
	run: (self: Suite, ...any) -> Promise<nil>,
}

local Suite = {}
Suite.__index = Suite

function Suite.new(): Suite
	local self = setmetatable({}, Suite)

	self.benchmarks = {}
	self.handlers = {}

	return (self :: any) :: Suite
end

function Suite:add(name, options)
	table.insert(self.benchmarks, {
		name = name,
		options = Object.assign({}, Benchmark.defaultOptions, Benchmark.options, options),
	})

	return self
end

function Suite:on(eventName, handler: (event: Event) -> ...any)
	self.handlers[eventName] = self.handlers[eventName] or {}
	table.insert(self.handlers[eventName], handler)
	return self
end

local function isSignificant(stats: Stats): boolean
	return stats.rme < MAX_RME
end

function Suite:run(...)
	return Promise.new(function(resolve)
		for i = 1, #self.benchmarks do
			local benchmark = Object.assign({}, self.benchmarks[i])
			local data = {}
			local name, options = benchmark.name, benchmark.options
			local maxTime = secToMs(options.maxTime)
			local startTime = secToMs(os.clock())
			while
				secToMs(os.clock()) - startTime < maxTime
				and (#data < options.minSamples or not isSignificant(benchmark.stats))
			do
				local cycleStartTime
				local cycleEndTime
				local ok, error_ = Promise.new(function(resolve)
					cycleStartTime = os.clock()
					options.fn({
						resolve = function(...)
							cycleEndTime = os.clock()
							resolve(...)
						end,
					})
				end):await()

				if not ok then
					notifyHandlers(benchmark, "error", {
						type = "error",
						target = benchmark,
						timestamp = secToMs(os.clock()),
						error = error_,
					})
					break
				end

				table.insert(data, cycleEndTime - cycleStartTime)

				benchmark.stats = calculateStats(data)

				local cycleEvent = {
					type = "cycle",
					target = benchmark,
					timestamp = secToMs(cycleEndTime),
				}
				notifyHandlers(benchmark, "cycle", cycleEvent)
				notifyHandlers(self, "cycle", cycleEvent)

				task.wait(options.delay)
			end
			notifyHandlers(benchmark, "complete", {
				type = "complete",
				target = benchmark,
				timestamp = secToMs(os.clock()),
			})

			printStats(name, data)
		end

		notifyHandlers(self, "complete", {
			type = "complete",
			target = self,
			timestamp = secToMs(os.clock()),
		})

		resolve()
	end):expect()
end

Benchmark.Suite = Suite
Benchmark.defaultOptions = {
	async = false,
	defer = false,
	delay = 0.005,
	id = nil,
	initCount = 1,
	maxTime = 5,
	minSamples = 5,
	minTime = 0,
	name = nil :: string?,
	onAbort = nil :: Function?,
	onComplete = nil :: Function?,
	onCycle = nil :: Function?,
	onError = nil :: Function?,
	onReset = nil :: Function?,
	onStart = nil :: Function?,
}
Benchmark.options = {} :: typeof(Benchmark.defaultOptions)
Benchmark.setOptions = function(options): ()
	for k, v in pairs(options) do
		Benchmark.options[k] = options[v]
	end
end

return Benchmark


-- ROBLOX no upstream

local rootWorkspace = script.Parent.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
local Array = LuauPolyfill.Array
local Boolean = LuauPolyfill.Boolean
type Array<T> = { [number]: T }

export type Stats = {
	min: number,
	max: number,
	mean: number,
	variance: number,
	sd: number,
	sem: number,
	df: number,
	critical: number,
	moe: number,
	rme: number,
	size: number,
}

--[[*
* T-Distribution two-tailed critical values for 95% confidence.
* For more info see http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm.
]]
local tTable = {
	["1"] = 12.706,
	["2"] = 4.303,
	["3"] = 3.182,
	["4"] = 2.776,
	["5"] = 2.571,
	["6"] = 2.447,
	["7"] = 2.365,
	["8"] = 2.306,
	["9"] = 2.262,
	["10"] = 2.228,
	["11"] = 2.201,
	["12"] = 2.179,
	["13"] = 2.16,
	["14"] = 2.145,
	["15"] = 2.131,
	["16"] = 2.12,
	["17"] = 2.11,
	["18"] = 2.101,
	["19"] = 2.093,
	["20"] = 2.086,
	["21"] = 2.08,
	["22"] = 2.074,
	["23"] = 2.069,
	["24"] = 2.064,
	["25"] = 2.06,
	["26"] = 2.056,
	["27"] = 2.052,
	["28"] = 2.048,
	["29"] = 2.045,
	["30"] = 2.042,
	["infinity"] = 1.96,
}

local function getMean(data: Array<number>)
	return Array.reduce(data, function(sum: number, a)
		return sum + a
	end, 0) / #data
end

local function getVariance(data: Array<number>, mean: number)
	return Array.reduce(data, function(sum: number, x)
		return sum + math.pow(x - mean, 2)
	end, 0) / (#data - 1)
end

local function calculateStats(data: Array<number>): Stats
	local size = #data
	-- Compute the sample mean (estimate of the population mean).
	local mean = getMean(data)
	-- Compute the sample variance (estimate of the population variance).
	local variance = getVariance(data, mean)
	-- Compute the sample standard deviation (estimate of the population standard deviation).
	local sd = math.sqrt(variance)
	-- Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean).
	local sem = sd / math.sqrt(size)
	-- Compute the degrees of freedom.
	local df = size - 1
	-- Compute the critical value.
	local idx = math.round(df)
	local critical = tTable[if Boolean.toJSBoolean(idx) then idx else 1] or tTable.infinity
	-- Compute the margin of error.
	local moe = sem * critical
	-- Compute the relative margin of error.
	local rme_ = (moe / mean) * 100
	local rme = if Boolean.toJSBoolean(rme_) then rme_ else 0

	return {
		min = math.min(math.huge, table.unpack(data)),
		max = math.max(0, table.unpack(data)),
		mean = mean,
		variance = variance,
		sd = sd,
		sem = sem,
		df = df,
		critical = critical,
		moe = moe,
		rme = rme,
		size = size,
	}
end

return calculateStats


local Root = script.Parent.ApolloClientBenchmarkModel
local ProcessService = game:GetService("ProcessService")

local Packages = Root.Packages
local benchmark = require(Packages.ApolloClientBenchmarks)

local ok, result = pcall(benchmark)

if not ok then
	warn(result)
	ProcessService:ExitAsync(1)
else
	ProcessService:ExitAsync(0)
end


local Root = game:GetService("ReplicatedStorage")

local Packages = Root.Packages

local runCLI = require(Packages.Dev.Jest).runCLI

local processServiceExists, ProcessService = pcall(function()
	return game:GetService("ProcessService")
end)

local status, result = runCLI(Root, {
	verbose = _G.verbose == "true",
	ci = _G.CI == "true",
	updateSnapshot = _G.UPDATESNAPSHOT == "true"
}, { Packages.ApolloClient }):awaitStatus()

if status == "Rejected" then
	print(result)
end

if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then
	if processServiceExists then
		ProcessService:ExitAsync(0)
	end
end

if processServiceExists then
	ProcessService:ExitAsync(1)
end

return nil


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
return {
	displayName = "ApolloClientLua",
	testMatch = { "**/*.spec" },
	setupFiles = { script.Parent.config.jest.setup },
}


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local srcWorkspace = script.Parent.Parent
local Packages = srcWorkspace.Parent
local LuauPolyfill = require(Packages.LuauPolyfill)
local Array, Object = LuauPolyfill.Array, LuauPolyfill.Object

local function equal(a: any, b: any): boolean
	if typeof(a) ~= typeof(b) then
		return false
	end
	if a == b then
		return true
	end
	if typeof(a) == "table" then
		if Array.isArray(a) and not Array.isArray(b) then
			return false
		end
		if #a ~= #b or #Object.keys(a) ~= #Object.keys(b) then
			return false
		end
		if Array.isArray(a) then
			for k, v in ipairs(a) do
				if not equal(a[k], b[k]) then
					return false
				end
			end
		else
			for k in pairs(a) do
				if not equal(a[tostring(k)], b[tostring(k)]) then
					return false
				end
			end
		end
		return true
	elseif typeof(a) == "string" or typeof(a) == "number" or typeof(a) == "boolean" then
		return a == b
	elseif typeof(a) == "function" then
		-- ROBLOX FIXME: best results so far for function comparison
		return debug.info(a, "n") == debug.info(b, "n")
			and debug.info(a, "s") == debug.info(b, "s")
			and debug.info(a, "l") == debug.info(b, "l")
			and debug.info(a, "a") == debug.info(b, "a")
	elseif typeof(a) == "thread" then
		error("unhandled equality check for type 'thread'")
	elseif typeof(a) == "userdata" then
		error("unhandled equality check for type 'userdata'")
	end
	error("unhandled equality check for type " .. typeof(a))
end

return equal


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local srcWorkspace = script.Parent.Parent
local rootWorkspace = srcWorkspace.Parent

local graphQLModule = require(rootWorkspace.GraphQL)
type DocumentNode = graphQLModule.DocumentNode
-- ROBLOX deviation: we're implementing TypedDocumentNode inline instead of importing it from a library
-- original: export { TypedDocumentNode } from '@graphql-typed-document-node/core';
export type TypedDocumentNode<Result = { [string]: any }, Variables = { [string]: any }> = DocumentNode
-- ROBLOX FIXME: intersecting types is forcing the key to be present even if it's optional. removing for now
--[[ *
    * This type is used to ensure that the variables you pass in to the query are assignable to Variables
    * and that the Result is assignable to whatever you pass your result to. The method is never actually
    * implemented, but the type is valid because we list it as optional
]]
-- & { __apiType: ((Variables) -> Result)? }

return {}


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local rootWorkspace = script.Parent.Parent.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)

-- ROBLOX TODO: include this in LuauPolyfill
local Object = LuauPolyfill.Object.assign({}, LuauPolyfill.Object, {
	getPrototypeOf = function(obj)
		local prototype = getmetatable(obj)
		return prototype ~= false and prototype or nil
	end,
	create = function(proto, properties)
		if properties == nil then
			properties = {}
		end
		return setmetatable(properties, proto)
	end,
})

return Object


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local HttpService = game:GetService("HttpService")

-- ROBLOX TODO: include in LuauPolyfill
local function encodeURIComponent(value: string): string
	local encoded = HttpService:UrlEncode(value)
	-- reverting encoded chars which are not encoded by JS
	local result = encoded
		:gsub("%%2D", "-")
		:gsub("%%5F", "_")
		:gsub("%%2E", ".")
		:gsub("%%21", "!")
		:gsub("%%7E", "~")
		:gsub("%%2A", "*")
		:gsub("%%27", "'")
		:gsub("%%28", "(")
		:gsub("%%29", ")")
	return result
end

return encodeURIComponent


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local function hasOwnProperty(obj: { [any]: any }, prop: string | number): boolean
	return obj[prop] ~= nil
end

return hasOwnProperty


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local rootWorkspace = script.Parent.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
local Object = LuauPolyfill.Object

local makeIntervalImpl = require(script.makeIntervalImpl)

return Object.assign({
	Object = require(script.Object),
}, makeIntervalImpl(delay))


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX TODO: move this to LuauPolyfill
local Status = newproxy(false)
type Status = typeof(Status)

type TaskStatus = number
type Task = { [Status]: TaskStatus }

local SCHEDULED = 1
local CANCELLED = 3

return function(delayImpl)
	local function setInterval(callback, intervalTime: number, ...): Task
		local args = { ... }
		local task = {
			[Status] = SCHEDULED,
		}

		-- delayTime is an optional parameter
		if intervalTime == nil then
			intervalTime = 0
		end

		-- To mimic the JS interface, we're expecting delayTime to be in ms
		local intervalTimeMs = intervalTime / 1000

		local delay_
		delay_ = function()
			delayImpl(intervalTimeMs, function()
				if task[Status] == SCHEDULED then
					callback(unpack(args))
					delay_()
				end
			end)
		end

		delay_()

		return task
	end

	local function clearInterval(task: Task)
		if task[Status] == SCHEDULED then
			task[Status] = CANCELLED
		end
	end

	return {
		setInterval = setInterval,
		clearInterval = clearInterval,
	}
end


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
return function<T, U>(obj: { [T]: U }, callback: (T) -> ())
	for key, _ in obj do
		callback(key)
	end
end


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local rootWorkspace = script.Parent.Parent.Parent

local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
local Array = LuauPolyfill.Array
local Object = LuauPolyfill.Object
local HttpService = game:GetService("HttpService")

function sortedEncode(toEncode: any)
	if not Array.isArray(toEncode) and typeof(toEncode) == "table" then
		local encodedKeyValues = Array.map(Array.sort(Object.keys(toEncode)), function(key)
			return HttpService:JSONEncode(key) .. ":" .. sortedEncode(toEncode[key])
		end)
		return "{" .. Array.join(encodedKeyValues, ",") .. "}"
	end
	return HttpService:JSONEncode(toEncode)
end

return {
	sortedEncode = sortedEncode,
}


--[[
 * Copyright (c) Roblox Corporation
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX comment: no upstream
-- ROBLOX TODO: replace when added to LuauPolyfill
local exports = {}

local function stringIndexOf(s: string, substring: string)
	local start, _end = string.find(s, substring)

	if start == nil then
		return -1
	else
		return start
	end
end

exports.stringIndexOf = stringIndexOf

return exports


--[[
 * Copyright (c) 2016 Ben Newman
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX no upstream, extracted from optimism/entry.ts to work around circular types/deps

local srcWorkspace = script.Parent.Parent
local rootWorkspace = srcWorkspace.Parent

local LuauPolyfill = require(rootWorkspace.LuauPolyfill)

type Array<T> = LuauPolyfill.Array<T>
type Set<T> = LuauPolyfill.Set<T>
type Map<K, V> = LuauPolyfill.Map<K, V>
type Object = LuauPolyfill.Object

--[[
  ROBLOX deviation: no way to handle tuple like types in Luau
  original type:
  type Value<T> =
  | []           // unknown
  | [T]          // known value
  | [void, any]; // known exception
]]
-- Since exceptions are cached just like normal values, we need an efficient
-- way of representing unknown, ordinary, and exceptional values.
export type Value<T> = Array<T | any>

-- ROBLOX FIXME: this is a workaround for the 'recursive type with different args' error, remove this once that's fixed
type _Entry = {
	-- ROBLOX deviation: original type: OptimisticWrapOptions<TArgs>["subscribe"]
	subscribe: (((...any) -> ()) | (() -> any))?,
	-- ROBLOX deviation: original type: Unsubscribable["unsubscribe"]
	unsubscribe: (() -> any)?,

	parents: Set<AnyEntry>,
	childValues: Map<AnyEntry, Value<any>>,

	-- When self Entry has children that are dirty, self property becomes
	-- a Set containing other Entry objects, borrowed from emptySetPool.
	-- When the set becomes empty, it gets recycled back to emptySetPool.
	dirtyChildren: Set<AnyEntry> | nil,

	dirty: boolean,
	recomputing: boolean,
	value: Value<any>,

	fn: (...any) -> any,

	peek: (self: _Entry) -> any | nil,
	recompute: (self: _Entry, args: any) -> any,
	setDirty: (self: _Entry) -> (),
	dispose: (self: _Entry) -> (),
	forget: (self: _Entry) -> (),
	-- ROBLOX deviation: Dep<any> made object to eliminate circular dependency
	deps: Set<Object> | nil,
	-- ROBLOX deviation: Dep<any> made object to eliminate circular dependency
	dependsOn: (self: _Entry, dep: Object) -> (),
	forgetDeps: (self: _Entry) -> (),
}

export type AnyEntry = _Entry

return {}


--[[
 * Copyright (c) 2016 Ben Newman
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX no upstream
local srcWorkspace = script.Parent.Parent
local rootWorkspace = srcWorkspace.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
type Set<T> = LuauPolyfill.Set<T>
type Object = LuauPolyfill.Object

local helpersTypesModule = require(script.Parent.helpers_types)
export type Unsubscribable = helpersTypesModule.Unsubscribable
local anyEntryTypesModule = require(script.Parent.anyEntryTypes)
type AnyEntry = anyEntryTypesModule.AnyEntry

export type Dep<TKey> = Set<AnyEntry> & {
	-- ROBLOX deviation: inlined here since we can't access type fields, add self param
	-- subscribe: OptimisticWrapOptions<[TKey]>["subscribe"]
	subscribe: ((...TKey) -> () | (() -> any))?,
} & Unsubscribable

return {}


--[[
 * Copyright (c) 2016 Ben Newman
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX no upstream

local srcWorkspace = script.Parent.Parent
local rootWorkspace = srcWorkspace.Parent

local depTypesModule = require(script.Parent.depTypes)
type Dep<TKey> = depTypesModule.Dep<TKey>

local LuauPolyfill = require(rootWorkspace.LuauPolyfill)

type Array<T> = LuauPolyfill.Array<T>
type Set<T> = LuauPolyfill.Set<T>
type Map<K, V> = LuauPolyfill.Map<K, V>
type Object = LuauPolyfill.Object

local anyEntryTypesModule = require(script.Parent.anyEntryTypes)
type AnyEntry = anyEntryTypesModule.AnyEntry

--[[
  ROBLOX deviation: no way to handle tuple like types in Luau
  original type:
  type Value<T> =
  | []           // unknown
  | [T]          // known value
  | [void, any]; // known exception
]]
-- Since exceptions are cached just like normal values, we need an efficient
-- way of representing unknown, ordinary, and exceptional values.
export type Value<T> = Array<T | any>

export type Entry<TArgs, TValue> = {
	-- ROBLOX deviation: original type: OptimisticWrapOptions<TArgs>["subscribe"]
	subscribe: (((...any) -> ()) | (() -> any))?,
	-- ROBLOX deviation: original type: Unsubscribable["unsubscribe"]
	unsubscribe: (() -> any)?,

	parents: Set<AnyEntry>,
	childValues: Map<AnyEntry, Value<any>>,

	-- When self Entry has children that are dirty, self property becomes
	-- a Set containing other Entry objects, borrowed from emptySetPool.
	-- When the set becomes empty, it gets recycled back to emptySetPool.
	dirtyChildren: Set<AnyEntry> | nil,

	dirty: boolean,
	recomputing: boolean,
	value: Value<TValue>,

	fn: (...any) -> TValue,

	peek: (self: Entry<TArgs, TValue>) -> TValue | nil,
	recompute: (self: Entry<TArgs, TValue>, args: TArgs) -> TValue,
	setDirty: (self: Entry<TArgs, TValue>) -> (),
	dispose: (self: Entry<TArgs, TValue>) -> (),
	forget: (self: Entry<TArgs, TValue>) -> (),
	deps: Set<Dep<any>> | nil,
	dependsOn: (self: Entry<TArgs, TValue>, dep: Dep<any>) -> (),
	forgetDeps: (self: Entry<TArgs, TValue>) -> (),
}

return {}


--[[
 * Copyright (c) 2021 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX deviation: this was to support webpack logic, logic which is needed in luau
local exports = {}
--
local _maybe = require(script.Parent.maybe).maybe
-- ROBLOX deviation: Roblox doesn't have a concept of window or process.env. _G is the replacement.
--[[
export default (
  maybe(() => globalThis) ||
  maybe(() => window) ||
  maybe(() => self) ||
  maybe(() => global) ||
  maybe(() => Function("return this")())
) as typeof globalThis & {
  __DEV__: typeof __DEV__;
};
]]

exports.default = _G

return exports


--[[
 * Copyright (c) 2021 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX deviation: this was to support webpack logic, logic which is needed in luau
local exports = {}
-- ROBLOX todo: this is supposed to be a generic function
local function maybe(thunk: () -> any): (any | nil)
	do --[[ ROBLOX COMMENT: try-catch block conversion ]]
		local _ok, result, hasReturned = xpcall(function()
			return thunk(), true
		end, function() end)
		if hasReturned then
			return result
		end
	end
	return nil
end
exports.maybe = maybe
return exports


--[[
 * Copyright (c) 2021 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
local srcWorkspace = script.Parent.Parent.Parent
local rootWorkspace = srcWorkspace.Parent

local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
local Boolean = LuauPolyfill.Boolean

local NULL = { __value = "null" }

export type NULL = typeof(NULL)

return {
	NULL = NULL,
	toJSBoolean = function(value)
		return Boolean.toJSBoolean(value) and value ~= NULL
	end,
}


--[[
 * Copyright (c) 2021 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX note: extracted from storeUtils to break circular dependencies
-- policies_types.lua:55:28-68: ApolloClient/Packages/ApolloClient/cache/inmemory/writeToStore_types -> ApolloClient/Packages/ApolloClient/cache/inmemory/types -> ApolloClient/Packages/ApolloClient/cache/inmemory/policies_types
local srcWorkspace = script.Parent.Parent.Parent
local rootWorkspace = srcWorkspace.Parent
local LuauPolyfill = require(rootWorkspace.LuauPolyfill)
type Array<T> = LuauPolyfill.Array<T>
type Object = LuauPolyfill.Object

local graphqlModule = require(rootWorkspace.GraphQL)
type VariableNode = graphqlModule.VariableNode

export type Reference = { __ref: string }
export type StoreValue = number | string | Array<string> | Reference | Array<Reference> | nil | Object
export type StoreObject = { __typename: string?, [string]: StoreValue }
export type Directives = { --[[ ROBLOX TODO: Unhandled node for type: TSIndexSignature ]]
	[string]: { -- ROBLOX note [directiveName: string]
		[string]: any, -- note ROBLOX [argName: string]
	},
}
export type VariableValue = (node: VariableNode) -> any
-- ROBLOX TODO: implement fragment module
-- local fragmentsModule = require(script.graphql.fragments)
-- exports.FragmentMap = fragmentsModule.FragmentMap
export type FragmentMap = { [string]: any }

return {}


--[[
 * Copyright (c) 2019-2021 Ben Newman <ben@eloper.dev>
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX no upstream

local contextModule = require(script.context)
export type Slot<T> = contextModule.Slot<T>

return contextModule


--[[
 * Copyright (c) 2019-2021 Ben Newman <ben@eloper.dev>
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
]]
-- ROBLOX no upstream

local trieModule = require(script.trie)

export type Trie<Data> = trieModule.Trie<Data>

return trieModule


--!native
--!optimize 2
--!strict

--[[
MIT License

Copyright (c) 2024 BusyCityGuy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

--[[
	Lune's `datetime` module uses lowerCamelCase, whereas Roblox's `DateTime` module uses UpperCamelCase.
	To allow for Roblox-style usage of DateTime objects, this module wraps Lune's `datetime` module to add
	Roblox-style properties and methods through the use of metatables.

	This module may need to be updated as different properties and methods are needed.
--]]

local LuneDateTime = require("@lune/datetime")

local DateTime = setmetatable({
	now = function()
		local self = LuneDateTime.now()
		return setmetatable({
			UnixTimestampMillis = self.unixTimestampMillis;
			UnixTimestamp = self.unixTimestamp;
		}, {__index = self})
	end;
}, {__index = LuneDateTime})

return DateTime


--!native
--!optimize 2
--!strict

--[[
MIT License

Copyright (c) 2024 BusyCityGuy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

--[[
	Jest depends on `loadmodule` existing on the `debug` api.
	On Roblox, it's locked behind a feature flag FFlagDebugLoadModule, which is not enabled by default.
	In Lune, it doesn't exist at all, so this module creates the interface for it but is not implemented.
	The _loader function needs to be overridden to be usable.
	In this project, the `lune/test.luau` script implements and sets the _loader function.
--]]

local Debug

Debug = setmetatable({
	_loader = function(_moduleScript: any)
		error("The loader for Debug.loadmodule is not implemented")
	end;

	loadmodule = function(moduleScript)
		return Debug._loader(moduleScript)
	end;
}, {__index = debug})

return Debug


--!optimize 2
--!strict

-- TODO: Reimplement actual Random library

export type PolyfillRandom = {
	NextInteger: (self: PolyfillRandom, minimum: number, maximum: number) -> number,
}
type Static = {
	new: (seed: number?) -> PolyfillRandom,
}

local Random = {} :: PolyfillRandom & Static;
(Random :: any).__index = Random

function Random.new(seed: number?)
	if seed then
		math.randomseed(seed)
	end
	return setmetatable({}, Random) :: never
end

function Random:NextInteger(minimum, maximum)
	return math.random(minimum, maximum)
end

return table.freeze(Random :: Static)


--!native
--!optimize 2
--!strict

local LunePolyfill = require("@packages/LunePolyfill")

local Utf8 = setmetatable({
	graphemes = LunePolyfill.Utf8.Graphemes;
}, {__index = utf8})

return Utf8


--!optimize 2
--!nonstrict

local TypeOf = require("@packages/TypeOf")

local Debug = {}
local TAB = "\t"

type GenericDictionary = {[string]: any}
type GenericTable = {[any]: any}

local Debug_Inspect: (...any) -> string

local REPLACERS = {
	["Index ?"] = "__index";
	["Newindex ?"] = "__newindex";
}

local function GetErrorData(formatString: string, ...) -- Make sure if you don't intend to format arguments in, you do %%f instead of %f
	if type(formatString) ~= "string" then
		error(GetErrorData("!The first parameter of error formatting must be a string", "Debug"))
	end

	local arguments = {...}
	local traceback = debug.traceback()
	local errorDepth = select(2, string.gsub(traceback, "\n", "")) - 2

	local prefix
	formatString, prefix = string.gsub(formatString, "^!", "", 1)

	local debugInfo = debug.info(errorDepth, "s")
	local moduleName = if not debugInfo
		then prefix == 1 and table.remove(arguments, 1) or "Command bar"
		else prefix == 1 and table.remove(arguments, 1) or table.remove(string.split(debugInfo, ".")) or "Command bar"

	local functionName = ""

	for index = 1, select("#", ...) do
		arguments[index] = Debug_Inspect(arguments[index])
	end

	for matched in string.gmatch(string.sub(traceback, 1, -11), "%- [^\r\n]+[\r\n]") do
		functionName = matched
	end

	functionName =
		string.gsub(string.gsub(string.sub(functionName, 3, -2), "%l+ (%S+)$", "%1"), " ([^\n\r]+)", " %1", 1)

	local index = 0
	for matched in string.gmatch(formatString, "%%%l") do
		index += 1
		if matched == "%q" then
			arguments[index] = string.gsub(arguments[index], " (%S+)$", " \"%1\"", 1)
		end
	end

	local success, errorString = pcall(
		string.format,
		"[%*] {%*} " .. string.gsub(formatString, "%%q", "%%*"),
		moduleName,
		REPLACERS[functionName] or functionName,
		table.unpack(arguments)
	)

	if success then
		return errorString, errorDepth
	end

	error(
		GetErrorData(
			"!Error formatting failed, perhaps try escaping non-formattable tags like so: %%%%f\n(Error Message): "
				.. errorString,
			"Debug"
		)
	)
end

function Debug.InspectFormat(formatString: string, ...)
	if type(formatString) ~= "string" then
		error(GetErrorData("!The first parameter of error formatting must be a string", "Debug"))
	end

	local arguments = {...}
	formatString = string.gsub(formatString, "^!", "", 1)

	for index = 1, select("#", ...) do
		arguments[index] = Debug_Inspect(arguments[index])
	end

	local index = 0
	for matched in string.gmatch(formatString, "%%%l") do
		index += 1
		if matched == "%q" then
			arguments[index] = string.gsub(arguments[index], " (%S+)$", " \"%1\"", 1)
		end
	end

	local success, errorString =
		pcall(string.format, (string.gsub(formatString, "%%q", "%%*")), table.unpack(arguments))

	if success then
		return errorString
	end

	error(
		GetErrorData(
			"!Error formatting failed, perhaps try escaping non-formattable tags like so: %%%%f\n(Error Message): "
				.. errorString,
			"Debug"
		)
	)
end

function Debug.Warn(...)
	warn((GetErrorData(...)))
end

function Debug.Error(...)
	error(GetErrorData(...))
end

function Debug.Assert(condition, ...)
	return condition or error(GetErrorData(...))
end

local function AssertNoTypes(condition, ...)
	return condition or error(GetErrorData(...))
end
Debug.AssertNoTypes = AssertNoTypes :: (...any) -> any

local function Alphabetically(a, b)
	local typeA = TypeOf(a)
	local typeB = TypeOf(b)

	if typeA == typeB then
		return if typeA == "number"
			then (a :: number) < b :: number
			else string.lower(tostring(a)) < string.lower(tostring(b))
	end

	return typeA < typeB
end

function Debug.AlphabeticalOrder<K, V>(dictionary: {[K]: V}): () -> (K, V)
	local order = {}
	local length = 0

	for key in dictionary do
		length += 1
		order[length] = key
	end

	table.sort(order, Alphabetically)

	local index = 0
	-- stylua: ignore
	return function(iteratedTable)
		index += 1
		local key = order[index]
		return key, iteratedTable[key], index
	end, dictionary, nil
end

function Debug.UnionIteratorFunctions(...)
	local iteratorFunctions = {...}

	for _, iteratorFunction in iteratorFunctions do
		if type(iteratorFunction) ~= "function" then
			error("Cannot union Iterator functions which aren't functions", 2)
		end
	end

	return function(iterateTable)
		local count = 0
		local order = {[0] = {}}
		local keysSeen = {}

		for index, iteratorFunction in iteratorFunctions do
			local callback, tableToIterateThrough, next = iteratorFunction(iterateTable)

			if type(callback) ~= "function" or type(tableToIterateThrough) ~= "table" then
				error(
					"Iterator function "
						.. index
						.. " must return a stack of types as follows: Function, Table, Variant",
					2
				)
			end

			while true do
				local data = {callback(tableToIterateThrough, next)}
				next = data[1]
				if next == nil then
					break
				end

				if not keysSeen[next] then
					keysSeen[next] = true
					count += 1
					table.insert(data, index)
					order[count] = data
				end
			end
		end

		-- stylua: ignore
		return function(_, previous)
			for index = 0, count do
				if order[index][1] == previous then
					local data = order[index + 1]
					if data then
						return table.unpack(data)
					end
					return nil
				end
			end

			error("invalid key to unioned iterator function: " .. previous, 2)
		end, iterateTable, nil
	end
end

local ConvertTableIntoString: (
	object: {[any]: any},
	tableName: string?,
	multiline: boolean?,
	depth: number,
	encounteredTables: {[any]: any}
) -> string

local function Parse(object: any, multiline: boolean?, depth: number, encounteredTables: {[any]: any})
	local typeOf = TypeOf(object)

	return typeOf == "table"
			and (encounteredTables[object] and "[table " .. encounteredTables[object] .. "]" or ConvertTableIntoString(
				object,
				nil,
				multiline,
				depth + 1,
				encounteredTables
			))
		or typeOf == "string" and "\"" .. object .. "\""
		-- or typeOf == "Instance" and "<" .. Debug.DirectoryToString(object) .. ">"
		or (typeOf == "function" or typeOf == "userdata") and typeOf
		or tostring(object)
end

function ConvertTableIntoString(object, tableName, multiline, depth, encounteredTables)
	local n = encounteredTables.n + 1
	encounteredTables[object] = n
	encounteredTables.n = n

	local array: {string} = {}
	local length = 1
	local currentArrayIndex = 1

	if tableName then
		array[1] = tableName
		array[2] = " = {"
		length = 2
	else
		array[1] = "{"
	end

	if not next(object) then
		array[length + 1] = "}"
		return table.concat(array)
	end

	for key, value in Debug.AlphabeticalOrder(object) do
		if not multiline and type(key) == "number" then
			if key == currentArrayIndex then
				currentArrayIndex += 1
			else
				length += 1
				array[length] = "[" .. key .. "] = "
			end

			length += 1
			array[length] = Parse(value, multiline, depth, encounteredTables)

			length += 1
			array[length] = ", "
		else
			if multiline then
				length += 1
				array[length] = "\n"

				length += 1
				array[length] = string.rep(TAB, depth)
			end

			if type(key) == "string" and string.find(key, "^[%a_][%w_]*$") then
				length += 1
				array[length] = key
			else
				length += 1
				array[length] = "["

				length += 1
				array[length] = Parse(key, multiline, depth, encounteredTables)

				length += 1
				array[length] = "]"
			end

			length += 1
			array[length] = " = "

			length += 1
			array[length] = Parse(value, multiline, depth, encounteredTables)

			length += 1
			array[length] = multiline and ";" or ", "
		end
	end

	if multiline then
		length += 1
		array[length] = "\n"

		length += 1
		array[length] = string.rep(TAB, depth - 1)
	else
		array[length] = nil
		length -= 1
	end

	length += 1
	array[length] = "}"

	local metatable = getmetatable(object :: never)

	if metatable then
		length += 1
		array[length] = " <- "

		length += 1
		array[length] = if type(metatable) == "table"
			then ConvertTableIntoString(metatable, nil, multiline, depth, encounteredTables)
			else Debug_Inspect(metatable)
	end

	return table.concat(array)
end

function Debug.TableToString<K, V>(object: {[K]: V}, multiline: boolean?, tableName: string?): string
	return ConvertTableIntoString(object, tableName, multiline, 1, {n = 0})
end

local ESCAPED_CHARACTERS = {"%", "^", "$", "(", ")", ".", "[", "]", "*", "+", "-", "?"}
local ESCAPABLE = "([%" .. table.concat(ESCAPED_CHARACTERS, "%") .. "])"

function Debug.EscapeString(value: string): string
	return (string.gsub(string.gsub(value, ESCAPABLE, "%%%1"), "([\"'\\])", "\\%1"))
end

function Debug_Inspect(...: any): string
	local variableArgumentsLength = select("#", ...)
	if variableArgumentsLength == 0 then
		return "NONE"
	end

	local stringBuilder = table.create(variableArgumentsLength * 2)
	local length = 0

	for index = 1, variableArgumentsLength do
		local data = select(index, ...)
		local dataType = TypeOf(data)
		local dataString

		if dataType == "EnumItem" then
			dataType = tostring(data.EnumType)
			dataString = tostring(data)
		elseif dataType == "EnumerationItem" then
			dataType = tostring(data.EnumerationType)
			dataString = tostring(data)
		else
			dataString = if dataType == "table"
				then Debug.TableToString(data)
				elseif dataType == "string" then "\"" .. data .. "\""
				else tostring(data)
		end

		stringBuilder[length + 1] = ", "
		stringBuilder[length + 2] =
			string.gsub(dataType .. " " .. dataString, "^" .. dataType .. " " .. dataType, dataType, 1)
		length += 2
	end

	return string.sub(table.concat(stringBuilder), 3)
end
Debug.Inspect = Debug_Inspect

return table.freeze(Debug)