-- written by groverbuger for g3d
-- february 2021
-- MIT license

local shader = require(G3D_PATH .. "/shader")
local newMatrix = require(G3D_PATH .. "/matrices")

----------------------------------------------------------------------------------------------------
-- define the camera singleton
----------------------------------------------------------------------------------------------------

local camera = {
    fov = math.pi/2,
    nearClip = 0.01,
    farClip = 1000,
    aspectRatio = love.graphics.getWidth()/love.graphics.getHeight(),
    position = {0,-10,0},
    target = {0,0,1},
    down = {0,-1,0},

    viewMatrix = newMatrix(),
    projectionMatrix = newMatrix(),
}

-- private variables used only for the first person camera functions
local fpsController = {
    direction = 0,
    pitch = 0
}

-- read-only variables, can't be set by the end user
function camera.getDirectionPitch()
    return fpsController.direction, fpsController.pitch
end

-- convenient function to return the camera's normalized look vector
function camera:getLookVector()
    local vx = camera.target[1] - camera.position[1]
    local vy = camera.target[2] - camera.position[2]
    local vz = camera.target[3] - camera.position[3]
    local length = math.sqrt(vx^2 + vy^2 + vz^2)

    -- make sure not to divide by 0
    if length > 0 then
        return vx/length, vy/length, vz/length
    end
    return vx,vy,vz
end

-- give the camera a point to look from and a point to look towards
function camera.lookAt(x,y,z, xAt,yAt,zAt)
    camera.position[1] = x
    camera.position[2] = y
    camera.position[3] = z
    camera.target[1] = xAt
    camera.target[2] = yAt
    camera.target[3] = zAt

    -- update the fpsController's direction and pitch based on lookAt
    -- thanks 4v0v!
    local dx,dy,dz = camera:getLookVector()
    fpsController.direction = math.pi/2 - math.atan2(dz, dx)
    fpsController.pitch = -math.atan2(dy, math.sqrt(dx^2 + dz^2))

    -- update the camera in the shader
    camera.updateViewMatrix()
end

-- move and rotate the camera, given a point and a direction and a pitch (vertical direction)
function camera.lookInDirection(x,y,z, directionTowards,pitchTowards)
    camera.position[1] = x or camera.position[1]
    camera.position[2] = y or camera.position[2]
    camera.position[3] = z or camera.position[3]

    fpsController.direction = directionTowards or fpsController.direction
    fpsController.pitch = pitchTowards or fpsController.pitch

    -- convert the direction and pitch into a target point

    -- turn the cos of the pitch into a sign value, either 1, -1, or 0
    local sign = math.cos(fpsController.pitch)
    sign = (sign > 0 and 1) or (sign < 0 and -1) or 0

    -- don't let cosPitch ever hit 0, because weird camera glitches will happen
    local cosPitch = sign*math.max(math.abs(math.cos(fpsController.pitch)), 0.00001)

    camera.target[1] = camera.position[1]+math.sin(fpsController.direction)*cosPitch
    camera.target[2] = camera.position[2]-math.sin(fpsController.pitch)
    camera.target[3] = camera.position[3]+math.cos(fpsController.direction)*cosPitch

    -- update the camera in the shader
    camera.updateViewMatrix()
end

-- recreate the camera's view matrix from its current values
-- and send the matrix to the shader specified, or the default shader
function camera.updateViewMatrix(shaderGiven)
    camera.viewMatrix:setViewMatrix(camera.position, camera.target, camera.down);
    (shaderGiven or shader):send("viewMatrix", camera.viewMatrix)
end

-- recreate the camera's projection matrix from its current values
-- and send the matrix to the shader specified, or the default shader
function camera.updateProjectionMatrix(shaderGiven)
    camera.projectionMatrix:setProjectionMatrix(camera.fov, camera.nearClip, camera.farClip, camera.aspectRatio);
    (shaderGiven or shader):send("projectionMatrix", camera.projectionMatrix)
end

-- recreate the camera's orthographic projection matrix from its current values
-- and send the matrix to the shader specified, or the default shader
function camera.updateOrthographicMatrix(size, shaderGiven)
    camera.projectionMatrix:setOrthographicMatrix(camera.fov, size or 5, camera.nearClip, camera.farClip, camera.aspectRatio);
    (shaderGiven or shader):send("projectionMatrix", camera.projectionMatrix)
end

-- simple first person camera movement with WASD
-- put this local function in your love.update to use, passing in dt
function camera.firstPersonMovement(dt)
    -- collect inputs
    local moveX,moveY = 0,0
    local cameraMoved = false
    local speed = 9
    if love.keyboard.isDown("w") then moveY = moveY - 1 end
    if love.keyboard.isDown("a") then moveX = moveX - 1 end
    if love.keyboard.isDown("s") then moveY = moveY + 1 end
    if love.keyboard.isDown("d") then moveX = moveX + 1 end


    if love.keyboard.isDown("space") then
        camera.position[2] = camera.position[2] - speed*dt
        cameraMoved = true
    end
    if love.keyboard.isDown("lshift") then
        camera.position[2] = camera.position[2] + speed*dt
        cameraMoved = true
    end
    
    -- do some trigonometry on the inputs to make movement relative to camera's direction
    -- also to make the player not move faster in diagonal directions
    if moveX ~= 0 or moveY ~= 0 then
        local angle = math.atan2(moveY,moveX)
        local directionX,directionZ = math.cos(fpsController.direction + angle)*speed*dt, math.sin(fpsController.direction + angle + math.pi)*speed*dt

        camera.position[1] = camera.position[1] + directionX
        camera.position[3] = camera.position[3] + directionZ
        cameraMoved = true
    end

    -- update the camera's in the shader
    -- only if the camera moved, for a slight performance benefit
    if cameraMoved then
        camera.lookInDirection()
    end
end

-- use this in your love.mousemoved function, passing in the movements
function camera.firstPersonLook(dx,dy)
    -- capture the mouse
    love.mouse.setRelativeMode(true)

    local sensitivity = 1/300
    fpsController.direction = fpsController.direction + dx*sensitivity
    fpsController.pitch = math.max(math.min(fpsController.pitch - dy*sensitivity, math.pi*0.5), math.pi*-0.5)

    camera.lookInDirection(camera.position[1],camera.position[2],camera.position[3], fpsController.direction,fpsController.pitch)
end


-- CUSTOM FUNCTIONS MODIFIED FROM ORIGINAL G3D

-- strategy game rts view
		-- movement: w,a,s,d, 
		-- rotate: q,e
		-- move by screen: 20 px: sides, forward, backwards
		-- mouse scroll: zoom
function camera.strategyViewMovement(dt,speed,is_scrolling)
    -- collect inputs
    local moveX,moveY = 0,0
    local cameraUpdate = false
    if is_scrolling then cameraUpdate = true end
    local speed = speed
    
    -- operate inputs
    if love.keyboard.isDown("w") then moveY = moveY - 1 end
    if love.keyboard.isDown("a") then moveX = moveX - 1 end
    if love.keyboard.isDown("s") then moveY = moveY + 1 end
    if love.keyboard.isDown("d") then moveX = moveX + 1 end   
       
    if love.keyboard.isDown("e") then 
    	camera.pitch = camera.pitch + 1/20
    	cameraUpdate = true
    end
    
    if love.keyboard.isDown("q") then 
    	camera.pitch = camera.pitch - 1/20
    	cameraUpdate = true
    end
        
    -- do some trigonometry on the inputs to make movement relative to camera's direction
    -- also to make the player not move faster in diagonal directions
    if moveX ~= 0 or moveY ~= 0 then
        local angle = math.atan2(moveY,moveX)
        local directionX,directionZ = math.cos(fpsController.direction + angle)*speed*dt, math.sin(fpsController.direction + angle + math.pi)*speed*dt

        camera.pivot[1] = camera.pivot[1] + directionX
        camera.pivot[3] = camera.pivot[3] + directionZ
        cameraUpdate = true
    end

   	
   camera.position[1] = camera.pivot[1] + math.cos(camera.pitch)*camera.zoom
   camera.position[2] = camera.pivot[2] - math.sin(45)*camera.zoom
   camera.position[3] = camera.pivot[3] + math.sin(camera.pitch)*camera.zoom
   	
    -- update the camera's in the shader
    -- only if the camera moved or rotated, for a slight performance benefit
    if cameraUpdate then
        camera.lookAt(camera.position[1],camera.position[2],camera.position[3],camera.pivot[1],camera.pivot[2],camera.pivot[3])
    end
end
return camera
