local preamble = [[
declare extern type userdata with end
declare extern type lightuserdata with end

declare extern type quaternion with
  x: number
  y: number
  z: number
  w: number
end

type Vec2 = {number}
type Vec3 = {number} | vector
type Vec4 = {number}
type Quat = {number} | quaternion
type Mat4 = {number}

declare class Joint end
declare class Shape end
]]

local function genType(type, optional)
  local suffix = optional and '?' or ''

  if type == 'function' then -- TODO
    return '() -> ()' .. suffix
  elseif type == '*' then
    return 'any' .. suffix
  else
    return type:gsub('table', '{}') .. suffix
  end
end

local function genArguments(arguments, ismethod)
  local t = {}

  for _, arg in ipairs(arguments) do
    local name, type = arg.name, genType(arg.type, arg.default)

    if name:match('%.%.%.') then
      if ismethod then
        table.insert(t, '...: ' .. type)
      else
        table.insert(t, '...' .. type)
      end
    else
      table.insert(t, ('%s: %s'):format(name, type))
    end
  end

  return table.concat(t, ', ')
end

local function genReturns(returns)
  local t = {}

  for _, ret in ipairs(returns) do
    table.insert(t, genType(ret.type))
  end

  return table.concat(t, ', ')
end

local function genFunctionType(fn, variant)
  local args = genArguments(variant.arguments)
  local rets = genReturns(variant.returns)

  if #variant.returns == 1 then
    return ('(%s) -> %s'):format(args, rets)
  else
    return ('(%s) -> (%s)'):format(args, rets)
  end
end

local function genMethod(method, variant)
  local args = genArguments(variant.arguments, true)
  local rets = genReturns(variant.returns)

  if args == '' then
    args = 'self'
  else
    args = 'self, ' .. args
  end

  if #variant.returns > 1 then
    rets = (': (%s)'):format(rets)
  elseif #variant.returns == 1 then
    rets = ': ' .. rets
  end

  return ('  function %s(%s)%s'):format(method.name, args, rets)
end

return function(api)
  local directory = lovr.filesystem.getSource() .. '/luau'

  if lovr.system.getOS() == 'Windows' then
    os.execute('mkdir ' .. directory:gsub('/', '\\'))
  else
    os.execute('mkdir -p ' .. directory)
  end

  local out = {}

  local function write(s, ...)
    table.insert(out, s:format(...))
  end

  write(preamble:gsub('^%s*', ''))
  write('\n')

  local function writeFunction(fn)
    if #fn.variants > 1 then
      write('  %s:\n', fn.name)

      for i, variant in ipairs(fn.variants) do
        write('    & (%s)%s\n', genFunctionType(fn, variant), i == #fn.variants and ',' or '')
      end
    else
      write('  %s: %s,\n', fn.name, genFunctionType(fn, fn.variants[1]))
    end
  end

  for _, module in ipairs(api.modules) do
    for _, enum in ipairs(module.enums) do
      write('type %s =\n', enum.name)
      for _, value in ipairs(enum.values) do
        write('  | %q\n', value.name)
      end
      write('\n')
    end

    local ignore = {
      Vec2 = true,
      Vec3 = true,
      Vec4 = true,
      Quat = true,
      Mat4 = true,
      Vectors = true
    }

    for _, object in ipairs(module.objects) do
      if not ignore[object.name] then
        write('declare class %s', object.name)

        if object.extends then
          write(' extends %s', object.extends)
        end

        write('\n')

        for _, method in ipairs(object.methods) do
          for _, variant in ipairs(method.variants) do
            write('%s\n', genMethod(method, variant))
          end
        end

        write('end\n\n')
      end
    end

    if module.name ~= 'lovr' and #module.functions > 0 then
      write('type %sModule = {\n', module.name:gsub('^%l', string.upper))

      for _, fn in ipairs(module.functions) do
        writeFunction(fn)
      end

      write('}\n\n')
    end
  end

  write('declare lovr: {\n')

  for _, module in ipairs(api.modules) do
    if module.name == 'lovr' then
      for _, fn in ipairs(module.functions) do
        writeFunction(fn)
      end
    end
  end

  write('\n')

  for _, callback in ipairs(api.callbacks) do
    writeFunction(callback)
  end

  write('\n')

  for _, module in ipairs(api.modules) do
    if module.name ~= 'lovr' and #module.functions > 0 then
      write('  %s: %sModule,\n', module.name, module.name:gsub('^%l', string.upper))
    end
  end

  write('}\n')

  local file = assert(io.open(directory .. '/lovr.d.luau', 'w'))
  file:write(table.concat(out):sub(1, -2))
  file:close()
end
