--# CardArranger
-- Orderly behaviour, layered on top of the loose physics rather than replacing
-- it. Nothing here teleports a card except at the very end of a move, when it
-- is already within a pixel or two of where it is going.
--
-- A card picked up anywhere but its ends squares itself to whichever of the
-- four quarter turns it was already nearest, so cards keep facing the way
-- they were facing unless they are deliberately spun.

CARD_QUARTER_STEP = 90

-- the shortest way round from one angle to another, in degrees, signed
function angleDifference(current, target)
  local difference = (target - current) % 360
  if difference > 180 then difference = difference - 360 end
  return difference
end

-- the nearest of the four square orientations, which is what a card locks to
-- when it is picked up: whichever way it was already facing
function nearestQuarterAngle(angle)
  return math.floor(angle / CARD_QUARTER_STEP + 0.5) * CARD_QUARTER_STEP
end

-- turns a body by driving its angular velocity, so it swings round rather than
-- snapping. returns true once it has arrived.
function driveAngle(body, target, speed)
  local difference = angleDifference(body.angle, target)
  if math.abs(difference) < 0.5 then
    body.angle = target
    body.angularVelocity = 0
    return true
  end
  body.angularVelocity = difference * (speed or 8)
  return false
end

-- Moves cards to places they have been told to go. Velocity is set rather than
-- position, so a card being arranged still shoves anything in its way, and a
-- finger can still take it back off the arranger mid flight.

CardArranger = class()

function CardArranger:init()
  self.tasks = {}
  self.gain = 6
  self.maxSpeed = 2400
  self.arriveDistance = 2
  self.maxTaskDuration = 2 -- force a task to finish rather than stay stuck forever
end

function CardArranger:sendTo(body, position, angle)
  self.tasks[body] = {position = position, angle = angle or 0, elapsed = 0}
end

function CardArranger:isArranging(body)
  return self.tasks[body] ~= nil
end

function CardArranger:cancel(body)
  self.tasks[body] = nil
end

function CardArranger:update()
  local finished = {}
  for body, task in pairs(self.tasks) do
    task.elapsed = task.elapsed + DeltaTime
    local toGo = task.position - body.position
    local distance = toGo:len()
    local placed = false
    if distance <= self.arriveDistance then
      body.position = task.position
      body.linearVelocity = vec2(0, 0)
      placed = true
    else
      body.linearVelocity = toGo:normalize() * math.min(distance * self.gain, self.maxSpeed)
    end
    local turned = driveAngle(body, task.angle, 8)
    if (placed and turned) or task.elapsed >= self.maxTaskDuration then
      body.position = task.position
      body.angle = task.angle
      body.linearVelocity = vec2(0, 0)
      body.angularVelocity = 0
      table.insert(finished, body)
    end
  end
  for _, body in ipairs(finished) do self.tasks[body] = nil end
end

-- every card sent to the same spot, which is what a stack is
function CardArranger:pile(cards, position)
  for _, card in ipairs(cards) do
    self:sendTo(card.body, position, 0)
  end
  return #cards > 0
end
