lovr = require 'lovr'

local lovr = lovr

local conf = {
  version = '0.19.0',
  identity = 'default',
  saveprecedence = true,
  modules = {
    audio = true,
    data = true,
    event = true,
    graphics = true,
    headset = true,
    math = true,
    physics = true,
    system = true,
    task = true,
    thread = true,
    timer = true
  },
  audio = {
    debug = false,
    start = true,
    reverb = {
      type = 'none',
      rays = 4096,
      bounces = 16,
      duration = 2,
      rate = .1
    }
  },
  graphics = {
    debug = false,
    vsync = true,
    stencil = false,
    antialias = true,
    lowpower = false,
    hdr = false,
    shadercache = true
  },
  headset = {
    connect = true,
    start = true,
    debug = false,
    seated = false,
    mask = true,
    stencil = false,
    antialias = true,
    supersample = false,
    dynamicresolution = true,
    submitdepth = true,
    overlay = false,
    controllerskeleton = 'controller',
    extensions = nil
  },
  math = {
    globals = true
  },
  thread = {
    workers = -1
  },
  window = {
    width = 1280,
    height = 800,
    centered = true,
    fullscreen = false,
    resizable = false,
    title = 'LÖVR',
    icon = nil
  }
}

function lovr.boot()
  lovr._setConf(conf)
  lovr.filesystem = require('lovr.filesystem')

  -- See if there's a ZIP archive fused to the executable, and set up the fused CLI if it exists

  local bundle, root = lovr.filesystem.getBundlePath()
  local fused = bundle and lovr.filesystem.mount(bundle, nil, true, root)
  local cli = lovr.filesystem.isFile('arg.lua') and assert(pcall(require, 'arg')) and lovr.arg and lovr.arg(arg)

  if cli == 'quit' then
    return function() return 0 end
  end

  -- Implement a barebones CLI if there is no bundled CLI/project

  if not fused then
    if arg[1] and not arg[1]:match('^%-') then
      for i = 0, #arg do
        arg[i - 1], arg[i] = arg[i], nil
      end
    else
      return function()
        print(table.concat({
          'usage: lovr <source>',
          '<source> can be a Lua file, a folder with a main.lua file, or a zip archive'
        }, '\n'))
        return 0
      end
    end
  end

  -- Figure out source archive and main module.  CLI places source at arg[0]

  local source, main
  if (cli or not fused) and arg[0] then
    if arg[0]:match('[^/\\]+%.lua$') then
      source = arg[0]:match('[/\\]') and arg[0]:match('(.+)[/\\][^/\\]+$') or '.'
      main = arg[0]:match('[^/\\]+%.lua$')
    else
      source = arg[0]
      main = 'main.lua'
    end
  elseif fused then
    source = bundle
    main = 'main.lua'
  end

  -- Mount source archive, make sure it's got the main file, and load conf.lua

  local ok, failure = true, nil
  if source ~= bundle and not lovr.filesystem.mount(source) then
    ok, failure = false, ('Failed to load project at %q\nMake sure the path or archive is valid.'):format(source)
  elseif not lovr.filesystem.isFile(main) then
    local location = source == '.' and '' or (' in %q'):format(source:match('[^/\\]+[/\\]?$'))
    ok, failure = false, ('No %s file found%s.\nThe project may be packaged incorrectly.'):format(main, location)
  else
    lovr.filesystem.setSource(source)
    local confPath = _VERSION == 'Luau' and lovr.filesystem.isFile('conf.luau') and 'conf.luau' or 'conf.lua'
    if lovr.filesystem.getRealDirectory(confPath) == source then
      ok, failure = pcall(require, 'conf')
      if ok and lovr.conf then
        ok, failure = pcall(lovr.conf, conf)
      end
    end
  end

  lovr.filesystem.setIdentity(conf.identity, conf.saveprecedence)

  -- CLI gets a chance to use/modify conf and handle arguments

  if ok and cli then ok, failure = pcall(cli, conf) end

  -- Boot!

  for module in pairs(conf.modules) do
    if conf.modules[module] then
      local loaded, result = pcall(require, 'lovr.' .. module)
      if not loaded then
        lovr.log(string.format('Could not load module %q: %s', module, result), 'warn')
      else
        lovr[module] = result
      end
    end
  end

  if lovr.headset and (not conf.headset or conf.headset.connect) then
    local ok, message = lovr.headset.connect()
    if not ok and conf.headset and conf.headset.debug then
      lovr.log(string.format('Could not connect to headset, falling back to simulator (%s)', message), 'warn', 'XR')
    end
  end

  if lovr.graphics then
    lovr.graphics.initialize()
  end

  if lovr.system and conf.window then
    lovr.system.openWindow(conf.window)
  end

  if lovr.headset and (not conf.headset or conf.headset.start) then
    lovr.headset.start()
  end

  if not ok and failure then
    error(failure)
  end

  require(main:sub(1, -5))

  return lovr.run()
end

function lovr.run()
  if lovr.timer then lovr.timer.step() end
  if lovr.load then lovr.load(arg) end
  return function()
    if lovr.headset then lovr.headset.pollEvents() end
    if lovr.system then lovr.system.pollEvents() end
    if lovr.event then
      for name, a, b, c, d in lovr.event.poll() do
        if name == 'restart' then return 'restart', lovr.restart and lovr.restart()
        elseif name == 'quit' and (not lovr.quit or not lovr.quit(a)) then return a or 0
        elseif name ~= 'quit' and lovr.handlers[name] then lovr.handlers[name](a, b, c, d) end
      end
    end
    local dt = 0
    if lovr.headset then lovr.headset.update() end
    if lovr.timer then dt = lovr.timer.step() end
    if lovr.headset and not lovr.headset.isActive() then lovr.simulate(dt) end
    if lovr.task then
      for task in lovr.task.poll() do
        lovr.taskready(task)
      end
    end
    if lovr.update then lovr.update(dt) end
    if lovr.audio then lovr.audio.update(dt) end
    if lovr.graphics then
      local window = lovr.graphics.getWindowPass()
      if lovr.headset then
        local headset = lovr.headset.getPass()
        if headset and lovr.draw and lovr.draw(headset) then headset = nil end
        if window and lovr.mirror and lovr.mirror(window) then window = nil end
        if headset or window then lovr.graphics.submit(headset, window) end
        lovr.headset.submit()
      elseif window and (not lovr.draw or not lovr.draw(window)) then
        lovr.graphics.submit(window)
      end
      lovr.graphics.present()
    elseif lovr.headset then
      lovr.headset.submit()
    end
  end
end

function lovr.mirror(pass)
  local texture = lovr.headset.getTexture()
  if texture then
    local w, h = pass:getDimensions()
    local tw, th = texture:getDimensions()
    local scale = tw / lovr.headset.getDisplayWidth()
    local cover = math.max(w / tw, h / th)
    pass:setProjection('orthographic')
    pass:setMaterial(texture)
    pass:plane(w / 2 * scale, h / 2 * scale, 0, tw * scale * cover, -th * scale * cover)
    pass:setMaterial()
  else
    return true
  end
end

local mouseX, mouseY, handX, handY, distance, pitch, yaw = nil, nil, 0, 0, .5, nil, nil

function lovr.simulate(dt)
  if not lovr.math then return end

  if not pitch or not yaw then
    pitch, yaw = quaternion(lovr.headset.getOrientation()):toeuler()
    mouseX, mouseY = lovr.system.getMousePosition()
  end

  local movespeed = 3
  local sprintspeed = 15
  local walkspeed = .5
  local turnspeed = .005
  local turnsmooth = 30

  local click = lovr.system.isMouseDown(1)

  lovr.system.setMouseMode(click and 'relative' or 'normal')

  local lastX, lastY = mouseX, mouseY
  mouseX, mouseY = lovr.system.getMousePosition()

  if click then
    yaw = yaw - (mouseX - lastX or mouseX) * turnspeed
    pitch = pitch - (mouseY - lastY or mouseY) * turnspeed
    pitch = math.min(pitch, math.pi / 2)
    pitch = math.max(pitch, -math.pi / 2)
  else
    handX, handY = mouseX, mouseY
  end

  local trigger = lovr.system.isMouseDown(2)
  lovr.headset.setButton('hand/left', 'trigger', trigger)
  lovr.headset.setButton('hand/left/point', 'trigger', trigger)

  -- Head

  local angle, ax, ay, az = lovr.headset.getOrientation()
  local target = quaternion(yaw, 0, 1, 0) * quaternion(pitch, 1, 0, 0)
  local orientation = quaternion(angle, ax, ay, az):slerp(target, 1 - math.exp(-turnsmooth * dt))

  local sprint = lovr.system.isKeyDown('lshift', 'rshift')
  local walk = lovr.system.isKeyDown('lctrl', 'rctrl')
  local forward = lovr.system.isKeyDown('w', 'up')
  local backward = lovr.system.isKeyDown('s', 'down')
  local left = lovr.system.isKeyDown('a', 'left')
  local right = lovr.system.isKeyDown('d', 'right')
  local up = lovr.system.isKeyDown('q')
  local down = lovr.system.isKeyDown('e')

  local vx = left and -1 or right and 1 or 0
  local vy = down and -1 or up and 1 or 0
  local vz = forward and -1 or backward and 1 or 0
  local speed = sprint and sprintspeed or walk and walkspeed or movespeed
  local velocity = vector(vx, vy, vz):normalize() * speed * dt
  local position = vector(lovr.headset.getPosition('head')) + orientation * velocity
  lovr.headset.setPose('head', position, orientation)

  -- Hand

  local left, right, up, down = lovr.headset.getViewAngles(1)
  local near, far = lovr.headset.getClipDistance()
  local inverseProjection = mat4():fov(left, right, up, down, near, far):invert()

  local width, height = lovr.system.getWindowDimensions()
  local coordinate = vector(handX / width * 2 - 1, handY / height * 2 - 1, 1, 1)
  local direction = (orientation * (inverseProjection * coordinate)):normalize()

  distance = distance * (1 + lovr.system._getScrollDelta() * .05)
  distance = math.min(distance, 10)
  distance = math.max(distance, .05)

  local handPosition = position + direction * distance
  local handOrientation = quaternion.lookdir(direction, orientation * vector.up)

  lovr.headset.setPose('hand/left', handPosition, handOrientation)
  lovr.headset.setPose('hand/left/point', handPosition, handOrientation)
end

function lovr.threaderror(thread, err)
  error('Thread error\n\n' .. err, 0)
end

function lovr.taskready(task)
  assert(lovr.task.resume(task))
end

function lovr.filechanged(path, action, oldpath)
  if not path:match('^%.') then
    lovr.event.restart()
  end
end

function lovr.log(message, level, tag)
  message = message:gsub('\n$', '')
  print(message)
end

lovr.handlers = setmetatable({}, { __index = lovr })

return coroutine.create(function()
  local function onerror(...)
    onerror = function(...)
      local traceback = debug and debug.traceback('', 1) or ''
      traceback = traceback:gsub('\n[^\n]+$', ''):gsub('\t', ''):gsub('stack traceback:', '\nStack:\n')
      print('Error:\n\n' .. tostring(...) .. traceback)
      return function() return 1 end
    end

    if not lovr.errhand and lovr.filesystem.isFile('errhand.lua') then
      pcall(require, 'errhand')
    end

    local ok, result = pcall(lovr.errhand or onerror, ...)

    if ok then
      return result or function() return 1 end
    else
      return onerror(result)
    end
  end

  local thread = select(2, xpcall(lovr.boot, onerror))

  while true do
    local ok, result, cookie = xpcall(thread, onerror)
    if not ok then thread = result
    elseif result then return result, cookie end
    coroutine.yield()
  end
end)
