local yeet = vec3()
local lightPerspective
local imat = mat4()
local blankMat = mat4()
local tempMat = mat4()
local tempMat2 = mat4()
local tempMat3 = mat4()

local function storeFreeMouse()
  mouse.preRelative = { x = mouse.x, y = mouse.y }
end

local function restoreFreeMouse()
  mouse.x = mouse.preRelative.x
  mouse.y = mouse.preRelative.y
  mouse_setPosition(mouse.preRelative.x, mouse.preRelative.y)
end

function camera_new(source)
  local cam = {
    isCamera = true,
    pos = vec3(),
    lookDir = vec3(),
    soom = 0.9,
    zoom = 1,
    shake = 0,
    fade = 0,
    focalL = 0,
    track = 0,
    blur = 0,
    forwardVelocity = 0,
    sideVelocity = 0,
    upVelocity = 0,
    wobbles = {},
    angle = { x = 0, y = 0, z = 0, tx = 0, ty = 0, tz = 0 },
    stored = { x = 0, y = 0 },
    offset = { x = 0, y = 0 },
    storedOffset = { x = 0, y = 0 },
    center = {
      x = 0,
      y = 0,
    },
    zoomBounds = { 0, 1 },
  }
  if source then
    -- print("source",inspect(source))
    for k, v in pairs(cam) do
      if source[k] then
        if k == "pos" then
          cam[k] = source[k]:clone()
        elseif type(v) == "table" then
          cam[k] = lume.clone(source[k])
        else
          cam[k] = source[k]
        end
      end
    end
  end

  return cam
end

function camera_project(position,camera)
  if not vec3.is_vec3(position) then error("not vec3") end
  local p, proj
  local view = ctx.viewport
  local camera = camera or view.camera

  if camera and camera.pos and view.bounds then
    if level.isAnim or vec3.dot(camera.pos - position, camera.lookDir) > 0 then
      proj = nil
    else
      p, proj =
        pcall(mat4.project, position, camera.projMatrix, camera.viewMatrix, { view.bounds.l, view.bounds.t, view.bounds.w, view.bounds.h })
    end
  end
  if p then
    local s = 100
    proj.y = view.bounds.h - proj.y
  -- if proj.x<-s or proj.x>view.bounds.w+s or proj.y<-s or proj.y>view.bounds.h+s then
  --   proj = false
  -- end
  else
    proj = nil
  end

  return proj
end

function camera_adjust(cam, view)
  bench.push("camera_adjust")
  if not cam.pos then
    cam.pos = vec3(cam.x, cam.y, cam.z)
    cam.x, cam.y, cam.z = nil, nil, nil
  end
  cam.viewMatrix = mat4.is_mat4(cam.viewMatrix) and cam.viewMatrix or mat4()

  local maxZoom = ctx.isAnim and (animatePanel.debugView and 0.77 or 0.81) or 0.85
  cam.zoomBounds = { -1.2, 1.2, 2.4 }

  cam.dropz = mc(view.room.zbounds.back, cam.pos.z, view.room.zbounds.front)
  --cam.focalL    = mc(0,cam.focalL,0.5)

  local soomMax = animatePanel.open and 1.22 or 1
  cam.soom = cam.soom > soomMax and easeTo(cam.soom, soomMax, 2, 5) or cam.soom < maxZoom and easeTo(cam.soom, maxZoom, 2, 5) or cam.soom
  cam.zoom = cam.soom * 6 * (1 + cam.focalL)
  cam.pfov = cam.fov or 0
  cam.fov = (cam.soom - 0.7) * (cam.ortho and 1200 or 360) - 20

  -- print("")
  if ctx.isAnim and animatePanel.open then
    if animatePanel.l.type == "drawing" and cam.angle.tx == 2 * pi and cam.angle.ty == 0 then
      cam.ortho = true
      -- cam.ortho = false
      cam.fov = (cam.soom ^ (8 * (animatePanel.debugZoom and 2 or 1.2))) * (screenH / 2)
    else
      cam.ortho = false
      cam.fov = 90
    end
    if animatePanel.exporting and animatePanel.l.type == "drawing" then
      cam.ortho = true
      cam.soom = 1
      cam.pos:set(0, 0, -145)
      cam.fov = (animatePanel.root.stageHeight / 2)
      cam.angle.tx, cam.angle.ty = 2 * pi, 0
    end
  end

  if view.bounds then
    local aspect = animatePanel.exporting and animatePanel.stageAspect or (view and view.bounds.w / view.bounds.h or game.aspectRatio)
    if aspect~= 0 then
      view.aspect = aspect
    end
    if cam.ortho then
      cam.projMatrix = mat4.from_ortho(-cam.fov * view.aspect, cam.fov * view.aspect, cam.fov, -cam.fov, -1000, 1000)
    else
      cam.projMatrix = mat4.from_perspective(cam.fov, view.aspect, ctx.isAnim and 1 or 1, ctx.isAnim and 1000 or 4000)
    end

    -- local preCam =  ctx.editing and cam or lume.merge(cam,scene_wobble(cam))

    shaders.forwardRender:send("cameraPosition", { cam.pos.x, cam.pos.y, cam.pos.z })

    cam.viewMatrix:identity()
    cam.viewMatrix:translate(cam.viewMatrix, -cam.pos)
    -- cam.viewMatrix:rotate(cam.viewMatrix, cam.angle.z, vec3.unit_z)
    cam.viewMatrix:rotate(cam.viewMatrix, -cam.angle.x, vec3.unit_y)
    cam.viewMatrix:rotate(cam.viewMatrix, -cam.angle.y + pi, vec3.unit_x)
    cam.vp = cam.vp or mat4()
    cam.vp = mat4.mul(cam.vp, cam.projMatrix, cam.viewMatrix)

    cam.ray = camera_cursorRay(mouse.x, mouse.y, view)
  end

  camera_orbit(cam)
  -- trace("  cam.focalL   "..round(cam.focalL,2))
  --    trace("  cam.tilt     "..round(cam.tilt,2))
     -- trace("  cam.ortho     "..tostring(cam.ortho))
  --    trace("  cam.soom     "..round(cam.soom,2))
  --    trace("  cam.zoom     "..round(cam.zoom))
  --    trace("  cam.izoom    "..round(cam.izoom,2))
  --    trace("  cam.fov      "..round(cam.fov, 2))

  if not light and editor.debug.trace_camera then
    trace("")

    trace("camera:  ", cam.pos)
    if cam.lookDir then trace(" cam.dir  ", cam.lookDir) end
    -- trace("  cam.focalL   "..round(cam.focalL,2))
    -- trace("  cam.tilt     "..round(cam.tilt,2))
    -- trace("  cam.soom     "..round(cam.soom,2))
    -- trace("  cam.zoom     "..round(cam.zoom))
    -- trace("  cam.izoom    "..round(cam.izoom,2))
    trace(" cam.fov    " .. tostring(cam.fov))

    trace("")
  end

  -- cam.lookDir = vec3.rotate(vec3.rotate(vec3(0,0,1), cam.angle.y , vec3.unit_x), pi-cam.angle.x , vec3.unit_y)
  bench.pop("camera_adjust")
end

function camera_pan(cam)
  mouse_setCursor(cursors.grab)

  if not cam.locked then
    if editor.panStarted then
      editor.panStarted = false
      cam.stored = { x = cam.pos.x, y = cam.pos.y, z = cam.pos.z, soom = cam.soom, b = b or 1 }
    end
    local soom = (cam.soom - 0.78) ^ 0.95

    if not shiftDown then
      if ctx.isAnim then
        animatePanel.panned = true
        cam.pos.x = cam.stored.x + (editor.mouseStored.x - mouse.x) * editor.panMult.x
        cam.pos.y = cam.stored.y + (editor.mouseStored.y - mouse.y) * editor.panMult.y
      else
        cam.pos.x = cam.stored.x - math.cos(cam.angle.x) * (editor.mouseStored.x - mouse.x) * 1 * soom
        cam.pos.z = cam.stored.z - math.sin(cam.angle.x) * (editor.mouseStored.x - mouse.x) * 1 * soom
        cam.pos.y = cam.stored.y + (editor.mouseStored.y - mouse.y) * 1 * soom
      end
    else
      if ctx.isAnim then
        cam.soom = mc(0.8, cam.stored.soom + (editor.mouseStored.y - mouse.y) / 2000, 1.22)
      else
        local a = cam.angle.x + pi
        cam.pos.x = cam.stored.x - math.cos(-a) * (editor.mouseStored.x - mouse.x) * 1 - math.sin(a) * (editor.mouseStored.y - mouse.y) * -1
        cam.pos.z = cam.stored.z - math.sin(-a) * (editor.mouseStored.x - mouse.x) * 1 - math.cos(a) * (editor.mouseStored.y - mouse.y) * -1
      end
    end
  end
end


function camera_orbit(cam)
  if editor.orbiting and editor.orbiting.target and not game.drawingShadow then
    trace("orbit target", editor.orbiting.target)
    editor.orbiting.ease = animatePanel.open and 1 or lerp(editor.orbiting.ease, 0.8, 0.04)
    local newx = pi / 2 + editor.orbiting.angx - (editor.orbiting.mouse.x - mouse.x) / 200
    local newy = editor.orbiting.angy + (mouse.y - editor.orbiting.mouse.y) / 200
    local pre = vec3.rotate(vec3(0, 0, 1), newy, vec3.unit_x)
    local new = editor.orbiting.target + vec3.rotate(pre, newx, vec3.unit_y) * editor.orbiting.dist
    local ease = editor.orbiting.ease
    cam.angle.x = lerpang(cam.angle.x, newx + pi, ease)
    cam.angle.tx = cam.angle.x
    cam.angle.y = lerpang(cam.angle.y, -newy, ease)
    cam.angle.ty = cam.angle.y
    while cam.angle.tx < 0 do
      cam.angle.tx = cam.angle.tx + pi * 2
      cam.angle.x = cam.angle.x + pi * 2
    end
    while cam.angle.tx > 2 * pi do
      cam.angle.tx = cam.angle.tx - pi * 2
      cam.angle.x = cam.angle.x - pi * 2
    end
    cam.pos:set(lerp(cam.pos.x, new.x, ease), lerp(cam.pos.y, new.y, ease), lerp(cam.pos.z, new.z, ease))

    if input_keyDown("w") then
      editor.orbiting.dist = editor.orbiting.dist * 0.97
    elseif input_keyDown("s") then
      editor.orbiting.dist = editor.orbiting.dist * 1.03
    end
    if mouse.scroll.y^2>0 then 
      editor.orbiting.dist = editor.orbiting.dist + mouse.scroll.y*-20
    end
  else
    cam.angle.x = easeTo(cam.angle.x, cam.angle.tx or cam.angle.x, 2, 20)
    cam.angle.y = easeTo(cam.angle.y, cam.angle.ty or cam.angle.y, 2, 20)

    while cam.angle.tx < 0 do
      cam.angle.tx = cam.angle.tx + pi * 2
      cam.angle.x = cam.angle.x + pi * 2
    end
    while cam.angle.tx > 2 * pi do
      cam.angle.tx = cam.angle.tx - pi * 2
      cam.angle.x = cam.angle.x - pi * 2
    end
  end

  if math.abs(cam.angle.tx - cam.angle.x) < 0.01 then cam.angle.x = cam.angle.tx end
  if math.abs(cam.angle.ty - cam.angle.y) < 0.01 then cam.angle.y = cam.angle.ty end
  cam.stopped = cam.angle.tx == cam.angle.x and cam.angle.ty == cam.angle.y
end

function camera_reset(cam, snap)
  cam.pos:set(vec3(0))
  if ctx.isAnim  then
    cam.pos.z = -100
    cam.pos.y = animatePanel.visible and animatePanel.height / 2 or 0
  end
  cam.angle.tx, cam.angle.ty = 2 * pi, 0

  if snap then
    cam.angle.x, cam.angle.y = cam.angle.tx, cam.angle.ty
  end
end

function camera_control(cam)
  if cmdDown and input_keyPressed("o") then cam.fov = cam.fov + 0.01 end
  if cmdDown and input_keyPressed("i") then cam.fov = cam.fov - 0.01 end

  if input_keyPressed("0") then
    if not ctx.isAnim and animatePanel.open then
      if cmdDown then
        --cam.pos:set(animatePanel.drawingPosition:unpack())
      end
      cam.angle.tx = math.angle(cam.pos.x, cam.pos.z, cam.lookTarg.x, cam.lookTarg.z) - pi / 2
      cam.angle.ty = math.angle(0, 0, math.dist(cam.pos.x, cam.pos.z, cam.lookTarg.x, cam.lookTarg.z), cam.lookTarg.y - cam.pos.y)
    else
      if cmdDown then camera_reset(cam) end
    end
    mouse_setRelativeMode(false)
  end

  mouse_pushClickR(function()
    if mouse_getRelativeMode() then
      mouse_setRelativeMode(false)
      restoreFreeMouse()
    end
  end)

  if editor.stageHovered then
    mouse_pushClick(function()
      if ctx.isAnim and animatePanel.l.type ~= "model" then return end
      local orbit
      if animatePanel.open and animatePanel.orbit then
        orbit = not altDown
      else
        orbit = altDown
      end
      if orbit then
        local target
        if animatePanel.open then
          -- if not editor.lastIntersect then
          -- if not animatePanel.orbitPosition then
          target = animatePanel.instance.position
        --  else
        --    target=animatePanel.orbitPosition
        --  end
        -- else
        --  animatePanel.orbitPosition = editor.lastIntersect
        --  target = animatePanel.orbitPosition
        -- end
        else
          target = editor.lastIntersect
        end

        if target then
          editor.orbiting = {
            ease = 0,
            mouse = vec2(mouse),
            start = cam.pos,
            base = cam.pos - target,
            angx = pi - math.angle(cam.pos.x, cam.pos.z, target.x, target.z),
            dist = vec3.dist(cam.pos, target),
            target = target,
          }
          editor.orbiting.angy = math.angle(0, 0, vec2.dist(vec2(target.x, target.z), vec2(cam.pos.x, cam.pos.z)), target.y - cam.pos.y)
        end
      end
    end)

    mouse_pushClickM(function()
      if ctx.isAnim and animatePanel.l.type ~= "model" then return end
      mouse_setRelativeMode(true)
      storeFreeMouse()
    end)
  end

  editor.mouseRelative = mouse_getRelativeMode() or editor.orbiting

  mouse_pushReleaseM(function()
    if editor.orbiting then editor.orbiting = nil end
    if mouse_getRelativeMode() and editor.stageHovered then restoreFreeMouse() end
    mouse_setRelativeMode(false)
  end)

  mouse_pushRelease(function()
    if editor.orbiting then editor.orbiting = nil end  
  end)

  if (input_keyPressed("tab")) and not (animatePanel.open or planPanel.open) then
    if mouse_getRelativeMode() then
      restoreFreeMouse()
    else
      storeFreeMouse()
    end
    mouse_setRelativeMode(not mouse_getRelativeMode())
    if type(cam.angle) == "number" then cam.angle = { x = 0, y = 0, z = 0, tx = 0, ty = 0, tz = 0 } end
  end

  if cmdDown and input_keyPressed("p") then
    local room = ctx.viewport.room
    room.lighting.enabled = true
    room.lighting.render.run = not room.lighting.render.run
    room.lighting.render.enabled = room.lighting.render.run
    -- editor.debug.draw_ground_planes = not editor.debug.draw_ground_planes
  end

  if mouse_getRelativeMode() then
    local slow = animatePanel and animatePanel.open
    local preSpeed = 200
    local altSpeed = slow and 0.2 or 3
    local target

    target = (leftAnalogVert or 0) + (input_keyDown("w") and 1 or (input_keyDown("s") and -1 or 0))
    cam.forwardVelocity = easeTo(cam.forwardVelocity or 0, target, (slow and math.abs(target) > math.abs(cam.forwardVelocity)) and 16 or 4)
    target = -(leftAnalogHori or 0) + (input_keyDown("d") and 1 or (input_keyDown("a") and -1 or 0))
    cam.sideVelocity = easeTo(cam.sideVelocity or 0, target, (slow and math.abs(target) > math.abs(cam.sideVelocity)) and 16 or 4)

    cam.speed = preSpeed * engine.dt * ((shiftDown or rightTrigButtonDown) and altSpeed or 1)

    cam.pos.x = cam.pos.x + math.sin(cam.angle.x) * cam.forwardVelocity * cam.speed 
                          + math.cos(-cam.angle.x) * cam.sideVelocity * cam.speed
    cam.pos.z = cam.pos.z + math.cos(cam.angle.x) * cam.forwardVelocity * cam.speed
                          + math.sin(-cam.angle.x) * cam.sideVelocity * cam.speed

    cam.upVelocity = easeTo(cam.upVelocity or 0, (input_keyDown("e") and -1 or (input_keyDown("q") and 1 or 0)), 4)
    cam.pos.y = cam.pos.y + cam.upVelocity * cam.speed
  end

  if mouse_getRelativeMode() then
    -- mouse_setPosition( mouse.preRelative.x, mouse.preRelative.y )
  end
end

function camera_scrollZoom(cam)
  if editor.stageHovered then
    if not (mouse.down.L or mouse.down.R) then
      --if input_keysModsTouched() then
      --  mouseScroll = {x=0,y=0}
      --end

      if cam.locked then
        if input_keyDown("space") then
          cam.offset.y = cam.offset.y + mouse.scroll.y * 5
          cam.offset.x = cam.offset.x - mouse.scroll.x * 5
        else
          cam.soom = cam.soom + mouse.scroll.y * 0.003
        end
      else
        cam.soom = cam.soom - mouse.scroll.y * 0.003
      end
    end
  end
end


function camera_cursorRay(x, y, view)
  local view = view or ctx.viewport
  local new = vec3()
  if not view.camera.pos or not (view.bounds and view.bounds.t) then return end

  local x = type(x) == "number" and x or mouse.x
  local y = type(y) == "number" and y or mouse.y

  -- y = y - view.bounds.t * 2

  local succ, unp

  succ, unp = pcall(mat4.unproject, vec3(x, y, 1), view.camera.projMatrix, { view.bounds.l, view.bounds.t, view.bounds.w, view.bounds.h })
  if unp then
    unp = unp / unp:len()
    new:set(unp.x, unp.y, -unp.z)
  -- trace("unproject ",unp.x,unp.y,-unp.z)
  else
    -- print("unproject failed")
  end

  if view.camera.ortho then
    -- TODO, this is incorrect
    new.z = 0
    new.x = view.camera.pos.x+(x-screenW/2)*(view.camera.fov/(screenH/2))
    new.y = view.camera.pos.y+(y-screenH/2)*(view.camera.fov/(screenH/2))
    return {
      position = new,
      direction = vec3(0, 0, 1000),
    }
  else
    new = vec3.rotate(vec3.rotate(new, view.camera.angle.y, vec3.unit_x), view.camera.angle.x, vec3.unit_y)
    view.camera.lookDir = vec3.rotate(vec3.rotate(vec3(0, 0, 1), view.camera.angle.y, vec3.unit_x), view.camera.angle.x, vec3.unit_y)
    -- trace("new ",new.x,new.y,new.z)
    return {
      position = view.camera.pos,
      direction = mouse_getRelativeMode() and view.camera.lookDir or new,
    }
  end
end