Shake = Object:extend()
function Shake:init(amplitude, duration, frequency)
  self.amplitude = amplitude or 0
  self.duration = duration or 0
  self.frequency = frequency or 60
  self.samples = {}
  for i = 1, (duration/1000)*frequency do self.samples[i] = 2*love.math.random()-1 end
  self.ti = love.timer.getTime()*1000
  self.t = 0
  self.shaking = true
end


function Shake:update(dt)
  self.t = love.timer.getTime()*1000 - self.ti
  if self.t > self.duration then
    self.shaking = false
  end
end


function Shake:get_noise(s)
  return self.samples[s] or 0
end


function Shake:get_decay(t)
  if t > self.duration then return end
  return (self.duration - t)/self.duration
end


function Shake:get_amplitude(t)
  if not t then
    if not self.shaking then return 0 end
    t = self.t
  end
  local s = (t/1000)*self.frequency
  local s0 = math.floor(s)
  local s1 = s0 + 1
  local k = self:get_decay(t)
  return self.amplitude*(self:get_noise(s0) + (s-s0)*(self:get_noise(s1) - self:get_noise(s0)))*k
end


-- The camera object. A global instance of this called "camera" is created automatically, and the Game class also contains its own instance of in the .camera attribute.
-- This class handles drawing of the game world through a viewport and the functions needed to make that happen.
-- A common setup looks like this:
--[[
function Arena:on_enter()
  game.camera.follow_style = 'lockon'
  game.camera.lerp.x = 0.1
  game.camera.lerp.y = 0.1
  
  self.main = Group(game.camera):set_as_physics_world(192, 0, 0, {'player', 'enemy', 'projectile', 'solid'})
  self.effects = Group(game.camera)
  self.floor = Group(game.camera)
  self.ui = Group()

  self.player = Player(gw/2, gh/2)
  self.main:add_object(self.player)
end

function Arena:update(dt)
  game.camera:follow_object(self.player)
  game.camera.x = math.floor(game.camera.x)
  game.camera.y = math.floor(game.camera.y)

  self.main:update(dt)
  self.floor:update(dt)
  self.effects:update(dt)
  self.ui:update(dt)
end

function Arena:draw()
  self.floor:draw()
  self.main:sort_by_y()
  self.main:draw()
  self.effects:draw()
  self.ui:draw()
end
]]--

-- Here we see 4 groups being created and for 3 of them the game's camera instance is attached.
-- If a group has a camera instance attached to it then whenever group:draw() is called it will draw all objects to the camera's canvas affected by the camera's translation, zoom and rotation.
-- Additionally, we also see some basic following functionality, as the camera is told to follow the player with the 'lockon' follow style and with some lerping going on.
-- Finally, the 4th group has no camera attached to it, because for UI elements we want them to be drawn in static screen positions.
--
-- The arguments passed in to create a camera area:
-- x, y - the camera's position in world coordinates, the camera is always centered around its x, y coordinates
-- w, h - the camera's size, generally this should be the same as game_width and game_height (or gw and gh) passed in engine_run
Camera = Object:extend()
function Camera:init(x, y, w, h)
  self.x, self.y = x, y
  self.w, self.h = w or gw, h or gh
  self.r, self.sx, self.sy = 0, 1, 1
  self.mouse = Vector(0, 0)
  self.last_mouse = Vector(0, 0)
  self.mouse_dt = Vector(0, 0)
  self.shakes = {x = {}, y = {}}
  self.spring = {x = Spring(), y = Spring()}
  self.lerp = Vector(1, 1)
  self.lead = Vector(1, 1)
  self.impulse = Vector(0, 0)
  self.follow_style = "no_deadzone"
  self.shake_amount = Vector(0, 0)
  self.last_shake_amount = Vector(0, 0)
  self.last_target = Vector(0, 0)
  self.scroll = Vector(0, 0)
end


-- Set ignore_scale to true when you want drawing of objects to be scaled when drawing the canvas rather than the objects themselves
-- That is useful for mipmapping a pixelated canvas, for instance, such that zooming out doesn't create lots of ugly artifacts
-- In that case you want to set ignore_scale to true, and then when drawing the main canvas multiply it by camera.sx*sx, camera.sy*sy instead of only sx, sy
-- You'll also want to create a canvas that is bigger than normal to account for the fact that you're zooming out of it
-- TODO: classify and unify all disparate methods of drawing in this class, also move "main canvas" here

-- Attaches the camera, meaning all further draw operations will be affected by its transform.
-- Accepts two values that go from 0 to 1 that represent how much parallaxing there should be for the next operations.
-- A value of 1 means no parallaxing, meaning the elements drawn will move at the same rate as all other elements, while a value of 0 means maximum parallaxing, which means the elements won't move at all.
-- Groups automatically call this function, but you can still pass these scroll factors to their draw calls.
-- In general if you have multiple layers of backgrounds and you want them to be parallaxed you'd do something like this:
--[[
function init()
  back_backgrounds = Group(game.camera)
  middle_backgrounds = Group(game.camera)
  front_backgrounds = Group(game.camera)
  main = Group(game.camera)
end

function draw()
  back_backgrounds:draw(0.5)
  middle_backgrounds:draw(0.75)
  front_backgrounds:draw(0.9)
  main:draw()
end
]]--
--
-- In this example we have 4 layers are all drawn with different levels of parallax: 0.5 (half speed), 0.75, 0.9 and 1 (normal speed = no parallax).
function Camera:attach(scroll_factor_x, scroll_factor_y)
  self.bx, self.by = self.x, self.y
  self.x = self.bx*(scroll_factor_x or 1)
  self.y = self.by*(scroll_factor_y or scroll_factor_x or 1)
  love.graphics.push()
  love.graphics.translate(self.w/2, self.h/2)
  if not self.ignore_scale then love.graphics.scale(self.sx, self.sy) end
  love.graphics.rotate(self.r)
  love.graphics.translate(-self.x*(scroll_factor_x or 1), -self.y*(scroll_factor_y or 1))
end


-- Detaches the camera, meaning all further draw operations won't be affected by its transform.
-- This is also called automatically by groups that have cameras attached to them.
function Camera:detach()
  love.graphics.pop()
  self.x, self.y = self.bx, self.by
end


-- Returns the values passed in in world coordinates. This is useful when transforming things from screen space to world space, like when the mouse clicks on something.
-- If you look at camera:get_mouse_position you'll see that it uses this function on the values returned by love.mouse.getPosition (which return values in screen coordinates).
-- camera:get_world_coords(love.mouse.getPosition())
function Camera:get_world_coords(x, y)
  local c, s = math.cos(-self.r), math.sin(-self.r)
  x, y = (x - sx*self.w/2)/(sx*self.sx), (y - sy*self.h/2)/(sy*self.sy)
  x, y = c*x - s*y, s*x + c*y
  return x + self.x*(v or 1), y + self.y*(v or 1)
end


-- Returns the values passed in in local coordinates. This is useful when transforming things from world space to screen space, like when displaying UI according to the position of game objects.
-- x, y = camera:get_local_coords(player.x, player.y)
function Camera:get_local_coords(x, y)
  local c, s = math.cos(self.r), math.sin(self.r)
  x, y = x - self.x*(v or 1), y - self.y*(v or 1)
  x, y = c*x - s*y, s*x + c*y
  return x*self.sx + self.w/2, y*self.sy + self.h/2
end


function Camera:update(dt)
  self.mouse.x, self.mouse.y = self:get_mouse_position()
  self.mouse_dt.x, self.mouse_dt.y = self.mouse.x - self.last_mouse.x, self.mouse.y - self.last_mouse.y
  self.shake_amount:set(0, 0)
  for _, z in ipairs({"x", "y"}) do
    for i = #self.shakes[z], 1, -1 do
      self.shakes[z][i]:update(dt)
      self.shake_amount[z] = self.shake_amount[z] + self.shakes[z][i]:get_amplitude()
      if not self.shakes[z][i].shaking then
        table.remove(self.shakes[z], i)
      end
    end
  end

  self.spring.x:update(dt)
  self.spring.y:update(dt)
  self.shake_amount:add(self.spring.x.x, self.spring.y.x)
  self.x, self.y = self.x - self.last_shake_amount.x, self.y - self.last_shake_amount.y
  self.x, self.y = self.x + self.shake_amount.x, self.y + self.shake_amount.y
  self.last_shake_amount:set(self.shake_amount)
  self.x = self.x + self.impulse.x*dt
  self.y = self.y + self.impulse.y*dt
  self.impulse:mul(0.9*refresh_rate*dt)

  if self.bound then
    self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
    self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
  end

  self.last_mouse.x, self.last_mouse.y = self.mouse.x, self.mouse.y
  if not self.target then return end

  if self.follow_style == "lockon" then
    local w, h = self.w/16, self.w/16
    self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
  elseif self.follow_style == "lockon_tight" then
    local w, h = self.w/64, self.w/64
    self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
  elseif self.follow_style == "lockon_loose" then
    local w, h = self.w/4, self.w/4
    self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
  elseif self.follow_style == "platformer" then
    local w, h = self.w/8, self.h/3
    self:set_deadzone((self.w - w)/2, (self.h - h)/2 - h*0.25, w, h)
  elseif self.follow_style == "topdown" then
    local s = math.max(self.w, self.h)/4
    self:set_deadzone((self.w - s)/2, (self.h - s)/2, s, s)
  elseif self.follow_style == "topdown_tight" then
    local s = math.max(self.w, self.h)/8
    self:set_deadzone((self.w - s)/2, (self.h - s)/2, s, s)
  elseif self.follow_style == "screen_by_screen" then
    self:set_deadzone(0, 0, 0, 0)
  elseif self.follow_style == "no_deadzone" then
    self.deadzone = nil
  end

  if not self.deadzone then
    self.x, self.y = self.target.x, self.target.y
    if self.bound then
      self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
      self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
    end
    return
  end

  local dx1, dy1, dx2, dy2 = self.deadzone.x, self.deadzone.y, self.deadzone.x + self.deadzone.w, self.deadzone.y + self.deadzone.h
  local scroll_x, scroll_y = 0, 0
  local target_x, target_y = self:get_local_coords(self.target.x, self.target.y)
  local x, y = self:get_local_coords(self.x, self.y)

  if self.follow_style == "screen_by_screen" then
    if self.bound then
      if self.x > self.bounds_min.x + self.w/2 and target_x < 0 then self.scroll.x = math.snap_center(self.scroll.x - self.w/self.sx, self.w/self.sx) end
      if self.x < self.bounds_max.x - self.w/2 and target_x >= self.w then self.scroll.x = math.snap_center(self.scroll.x + self.w/self.sx, self.w/self.sx) end
      if self.y > self.bounds_min.y + self.h/2 and target_y < 0 then self.scroll.y = math.snap_center(self.scroll.y - self.h/self.sy, self.h/self.sy) end
      if self.y < self.bounds_max.y - self.h/2 and target_y >= self.h then self.scroll.y = math.snap_center(self.scroll.y + self.h/self.sy, self.h/self.sy) end
    else
      if target_x < 0 then self.scroll.x = math.snap_center(self.scroll.x - self.w/self.sx, self.w/self.sx) end
      if target_x >= self.w then self.scroll.x = math.snap_center(self.scroll.x + self.w/self.sx, self.w/self.sx) end
      if target_y < 0 then self.scroll.y = math.snap_center(self.scroll.y - self.h/self.sy, self.h/self.sy) end
      if target_y >= self.h then self.scroll.y = math.snap_center(self.scroll.y + self.h/self.sy, self.h/self.sy) end
    end
    self.x = math.lerp(self.lerp.x, self.x, self.scroll.x)
    self.y = math.lerp(self.lerp.y, self.y, self.scroll.y)

    if self.bound then
      self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
      self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
    end

  else
    if target_x < x + (dx1 + dx2 - x) then
      local d = target_x - dx1
      if d < 0 then scroll_x = d end
    end
    if target_x > x - (dx1 + dx2 - x) then
      local d = target_x - dx2
      if d > 0 then scroll_x = d end
    end
    if target_y < y + (dy1 + dy2 - y) then
      local d = target_y - dy1
      if d < 0 then scroll_y = d end
    end
    if target_y > y - (dy1 + dy2 - y) then
      local d = target_y - dy2
      if d > 0 then scroll_y = d end
    end

    if not self.last_target.x and not self.last_target.y then self.last_target.x, self.last_target.y = self.target.x, self.target.y end
    scroll_x = scroll_x + (self.target.x - self.last_target.x)*self.lead.x
    scroll_y = scroll_y + (self.target.y - self.last_target.y)*self.lead.y
    self.last_target.x, self.last_target.y = self.target.x, self.target.y
    self.x = math.lerp(self.lerp.x, self.x, self.x + scroll_x)
    self.y = math.lerp(self.lerp.y, self.y, self.y + scroll_y)

    if self.bound then
      self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
      self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
    end
  end
end


-- Shakes the camera with a certain intensity for duration seconds and with the specified frequency
-- Higher frequency means jerkier movement, lower frequency means smoother movement
-- camera:shake(10, 1, 120) -> shakes the camera with 10 intensity for 1 second and 120 frequency
function Camera:shake(intensity, duration, frequency)
  table.insert(self.shakes.x, Shake(intensity, 1000*(duration or 0), frequency or 60))
  table.insert(self.shakes.y, Shake(intensity, 1000*(duration or 0), frequency or 60))
end


-- Shakes the camera with a certain intensity towards angle r using a spring mechanism
-- k and d are stiffness and damping spring values (see spring file for more)
-- camera:shake(10, math.pi/4) -> shakes the camera with 10 intensity diagonally
function Camera:spring_shake(intensity, r, k, d)
  self.spring.x:pull(-intensity*math.cos(r or 0), k, d)
  self.spring.y:pull(-intensity*math.sin(r or 0), k, d)
end


-- When following an object, this function sets a deadzone in which the camera can move freely in before having to go back to following the object
-- TODO: add STALKER-X documentation examples of all deadzones and lerping modes here
function Camera:set_deadzone(x, y, w, h)
  self.deadzone = {x = x, y = y, w = w, h = h}
end


-- Sets the boundaries of the camera as a rectangle.
-- The camera's center will not be allowed to move outside the boundaries of this rectangle.
-- TODO: this doesn't seem to work when zooming levels are different than 1, fix it
function Camera:set_bounds(x, y, w, h)
  self.bound = true
  self.bounds_min = {x = x - w/2, y = y - h/2}
  self.bounds_max = {x = x + w/2, y = y + h/2}
end


-- Tells the camera to follow the object
-- Previously set lerp, lead, follow styles and deadzone variables apply when following the object
function Camera:follow_object(obj)
  self.target = obj
end


-- Returns the mouse's position in world coordinates
-- x, y = camera:get_mouse_position()
function Camera:get_mouse_position()
  return self:get_world_coords(love.mouse.getPosition())
end


-- Returns the angle from a point to the mouse
-- x, y = camera:angle_to_mouse(point.x, point.y)
function Camera:angle_to_mouse(x, y)
  local mx, my = self:get_mouse_position()
  return math.angle(x, y, mx, my)
end


function Camera:apply_impulse(f, r)
  self.impulse:set(f*math.cos(r), f*math.sin(r))
end
