--# CardShelves
-- A shelf is a hand of cards held along one edge of the screen. Cards sit
-- mostly off the screen, with only their top edge peeking in.
--
-- A retracted shelf peeks by a little more than a tap target, so any card in
-- it can still be tapped, and draws no strip at all. The shelf whose cards
-- are face up (Shelves.shownShelf), or one a drag is aiming at, expands: its
-- cards slide further in and its coloured strip fades up behind them.
--
-- Tapping a card in the shown shelf selects it. A selected card lifts further
-- in than its neighbours and pushes them aside a little, so more of its face
-- shows without it leaving the fan.
--
-- A shelved card is still an ordinary physics body. It just stops hitting
-- things and gets pinned to its slot each frame.

Shelf = class()

Shelf.inwardOf = {bottom = vec2(0, 1), top = vec2(0, -1), left = vec2(1, 0), right = vec2(-1, 0)}
Shelf.angleOf = {bottom = 0, top = 180, left = -90, right = 90}
Shelf.colorOf = {
  bottom = color(255, 209, 60, 210),
  top = color(255, 149, 43, 210),
  left = color(95, 201, 112, 210),
  right = color(173, 112, 224, 210)
}
Shelf.minShownFraction = 0.75
Shelf.expandSpeed = 5 --how fast the shelf slides open and shut, per second

function Shelf:init(edge, cardTable)
  self.edge = edge
  self.cardTable = cardTable
  self.cards = {}
  self.alongFor = {}
  self.popFor = {}
  self.selected = nil
  self.expandAmount = 0
  self.isAimed = false
end

function Shelf:isEmpty()
  return #self.cards == 0
end

function Shelf:runsAcross()
  return self.edge == "bottom" or self.edge == "top"
end

-- a shelf lying along the screen's shorter side has less room to work with,
-- so it gets half the side padding of one along a long side
function Shelf:isOnShortSide()
  return self:fullLength() <= math.min(WIDTH, HEIGHT)
end

-- how much of a card peeks in when the shelf is shut: enough to tap
function Shelf:retractedDepth()
  return tapTargetSize()
end

-- and when it is open, enough to actually read the cards
function Shelf:expandedDepth()
  return math.max(self.cardTable:cardHeight() * 0.5, self:retractedDepth() * 1.5)
end

-- how much of a card is on screen right now, part way through the animation
function Shelf:visibleDepth()
  local eased = self:easedExpand()
  return self:retractedDepth() + (self:expandedDepth() - self:retractedDepth()) * eased
end

-- the length of the whole side this shelf runs along
function Shelf:fullLength()
  if self:runsAcross() then return WIDTH end
  return HEIGHT
end

-- the two perpendicular shelves eat into this shelf's ends, so the space
-- actually available for the strip and its cards is shorter than the side
function Shelf:cornerReserve()
  return self:expandedDepth() + self.cardTable:cardWidth() * 0.15
end

function Shelf:usableLength()
  return math.max(self.cardTable:cardWidth(), self:fullLength() - self:cornerReserve() * 2)
end

-- a little breathing room so the fan doesn't touch the strip's own edges
function Shelf:sidePadding()
  local padding = self.cardTable:cardWidth() * 0.15
  if self:isOnShortSide() then padding = padding * 0.5 end
  return padding
end

-- the length a comfortable, uncompressed spread of the current cards needs
function Shelf:neededLength()
  local footprint = self.cardTable:cardWidth()
  if #self.cards <= 1 then return footprint end
  return footprint + (footprint * 0.6) * (#self.cards - 1)
end

-- never narrower than the minimum fraction, never wider than the usable span
function Shelf:displayLength()
  local usable = self:usableLength()
  return math.min(usable, math.max(usable * Shelf.minShownFraction, self:neededLength()))
end

function Shelf:distanceFrom(position)
  if self.edge == "bottom" then return position.y end
  if self.edge == "top" then return HEIGHT - position.y end
  if self.edge == "left" then return position.x end
  return WIDTH - position.x
end

-- the coordinate that moves along the shelf's own axis: x for a shelf that
-- runs across the screen, y for one that runs up it
function Shelf:alongOf(position)
  if self:runsAcross() then return position.x end
  return position.y
end

-- keeps a dragged card's along-position within this shelf's own span, so it
-- can never be pulled into the corner space reserved for the neighbours
function Shelf:clampAlong(along)
  local half = self:displayLength() * 0.5
  local center = self:fullLength() * 0.5
  return math.max(center - half, math.min(center + half, along))
end

-- the centre of a card sitting in the shelf. only visibleDepth of the card
-- is on screen; the rest hangs off the edge. lift pulls a selected card
-- further in than its neighbours.
function Shelf:slotAt(along, lift)
  local sunk = self:visibleDepth() + (lift or 0) - (self.cardTable:cardHeight() * 0.5)
  local inward = Shelf.inwardOf[self.edge]
  if self:runsAcross() then
    local y = 0
    if self.edge == "top" then y = HEIGHT end
    return vec2(along, y) + inward * sunk
  end
  local x = 0
  if self.edge == "right" then x = WIDTH end
  return vec2(x, along) + inward * sunk
end

-- the starting "along" position and the spacing between cards for a given
-- card count, tightening the spacing if that many cards would not otherwise
-- fit in the padded, corner-safe display length
function Shelf:layoutSpan(count)
  local footprint = self.cardTable:cardWidth()
  local room = math.max(footprint, self:displayLength() - self:sidePadding() * 2)
  local spacing = footprint * 0.6
  if count > 1 then
    spacing = math.min(spacing, (room - footprint) / (count - 1))
  end
  local start = (self:fullLength() * 0.5) - (spacing * (count - 1) * 0.5)
  return start, spacing
end

function Shelf:indexOf(card)
  for i, c in ipairs(self.cards) do
    if c == card then return i end
  end
  return nil
end

-- neighbours nearest the selected card move most, the rest taper off
function Shelf:accordionFalloff(distance)
  return 1 / distance
end

function Shelf:selectionShift()
  return self.cardTable:cardWidth() * 0.2
end

function Shelf:selectionLift()
  return self.cardTable:cardHeight() * 0.12
end

-- works out where every card belongs, stores it, and starts anything that is
-- not already there moving. excludeCard is skipped, which is used while that
-- one card is being driven by a finger instead.
function Shelf:layOut(excludeCard)
  if self:isEmpty() then return end
  local start, spacing = self:layoutSpan(#self.cards)
  local selectedIndex = nil
  if self.selected then selectedIndex = self:indexOf(self.selected) end
  for i, card in ipairs(self.cards) do
    local along = start + spacing * (i - 1)
    local lift = 0
    if selectedIndex then
      if i < selectedIndex then
        along = along - self:selectionShift() * self:accordionFalloff(selectedIndex - i)
      elseif i > selectedIndex then
        along = along + self:selectionShift() * self:accordionFalloff(i - selectedIndex)
      else
        lift = self:selectionLift()
      end
    end
    self.alongFor[card] = along
    self.popFor[card] = lift
    if card ~= excludeCard then
      self.cardTable.arranger:sendTo(card.body, self:slotAt(along, lift), Shelf.angleOf[self.edge])
    end
  end
end

-- self.cards is in along-order, so drawing them in that order is what makes
-- the rightmost card sit on top. the selected card goes last of all.
function Shelf:showOnTop()
  self.cardTable:syncShelfDrawOrder(self, self.selected)
end

function Shelf:setFaceUp(faceUp)
  for _, card in ipairs(self.cards) do
    if faceUp then card:faceUp() else card:faceDown() end
  end
  if not faceUp then self:setSelected(nil) end
end

function Shelf:setSelected(card)
  if self.selected == card then return end
  self.selected = card
  self:layOut()
  self:showOnTop()
end

-- where in the fan a card dropped at this along-position belongs
function Shelf:indexForAlong(along)
  local start, spacing = self:layoutSpan(#self.cards + 1)
  for i = 1, #self.cards do
    if along < start + spacing * (i - 1) then return i end
  end
  return #self.cards + 1
end

-- atAlong, if given, puts the incoming cards where they were dropped rather
-- than on the end of the fan
function Shelf:accept(cards, atAlong)
  for _, card in ipairs(cards) do
    if not card.body.shelf then
      card.body.shelf = self
      card.body.savedMask = card.body.mask
      lab:canHitNothing(card.body)
      if atAlong then
        table.insert(self.cards, self:indexForAlong(atAlong), card)
      else
        table.insert(self.cards, card)
      end
    end
  end
  self:setFaceUp(self.cardTable.shelves.shownShelf == self)
  self:layOut()
  self:showOnTop()
end

-- a card leaving keeps whatever angle it had; nothing is straightened here
function Shelf:release(card)
  for i = #self.cards, 1, -1 do
    if self.cards[i] == card then table.remove(self.cards, i) end
  end
  if self.selected == card then self.selected = nil end
  card.body.shelf = nil
  card.body.mask = card.body.savedMask or {1}
  card.body.savedMask = nil
  self.alongFor[card] = nil
  self.popFor[card] = nil
  self.cardTable.arranger:cancel(card.body)
  self:layOut()
  self:showOnTop()
end

-- move card to wherever along sits among this shelf's other cards, then
-- relay out everyone except card, which the finger is driving directly
function Shelf:reorderTo(card, along)
  local without = {}
  for _, c in ipairs(self.cards) do
    if c ~= card then table.insert(without, c) end
  end
  local start, spacing = self:layoutSpan(#self.cards)
  local insertIndex = #without + 1
  for i, c in ipairs(without) do
    if along < start + spacing * (i - 1) then
      insertIndex = i
      break
    end
  end
  table.insert(without, insertIndex, card)
  self.cards = without
  self:layOut(card)
  self.cardTable:syncShelfDrawOrder(self, card)
end

-- cards are pinned to their slot every frame, which is also what animates
-- them in and out as the shelf opens and shuts
function Shelf:update()
  self:updateExpand()
  for _, card in ipairs(self.cards) do
    if not self.cardTable.arranger:isArranging(card.body) then
      local along = self.alongFor[card]
      if along then
        card.body.position = self:slotAt(along, self.popFor[card] or 0)
      end
      card.body.linearVelocity = vec2(0, 0)
      card.body.angularVelocity = 0
      card.body.angle = Shelf.angleOf[self.edge]
    end
  end
end

function Shelf:shouldExpand()
  return self.isAimed or self.cardTable.shelves.shownShelf == self
end

function Shelf:updateExpand()
  local target = 0
  if self:shouldExpand() then target = 1 end
  local step = DeltaTime * Shelf.expandSpeed
  if self.expandAmount < target then
    self.expandAmount = math.min(target, self.expandAmount + step)
  elseif self.expandAmount > target then
    self.expandAmount = math.max(target, self.expandAmount - step)
  end
end

-- ease-out cubic, so the slide is quick at first and settles gently
function Shelf:easedExpand()
  local t = 1 - self.expandAmount
  return 1 - (t * t * t)
end

-- the strip's rect, sized to whatever the shelf is showing right now
function Shelf:visualRect()
  local depth = self:visibleDepth()
  local length = self:displayLength()
  local radius = math.min(depth, length) * 0.25
  if self:runsAcross() then
    local x = (WIDTH - length) * 0.5
    local y = 0
    if self.edge == "top" then y = HEIGHT - depth end
    return x, y, length, depth, radius
  end
  local y = (HEIGHT - length) * 0.5
  local x = 0
  if self.edge == "right" then x = WIDTH - depth end
  return x, y, depth, length, radius
end

-- whether a touch lands on this shelf's band of the screen
function Shelf:containsTouch(touch)
  if self:distanceFrom(touch.pos) > self:visibleDepth() then return false end
  local along = self:alongOf(touch.pos)
  local half = self:displayLength() * 0.5
  return math.abs(along - self:fullLength() * 0.5) <= half
end

-- a retracted shelf draws no strip at all, so the cards look like they are
-- simply resting off the edge of the table
function Shelf:draw()
  if self.expandAmount <= 0 then return end
  local eased = self:easedExpand()
  local x, y, w, h, radius = self:visualRect()
  local base = Shelf.colorOf[self.edge]
  local fillCol = color(base.r, base.g, base.b, base.a * eased)
  local strokeCol, strokeW = color(0, 0, 0, 0), 0
  if self.isAimed then
    strokeCol = color(255, 255, 255, 220 * eased)
    strokeW = 3
  end
  drawRoundedRect(x, y, w, h, radius, fillCol, strokeCol, strokeW)
end

-- The four of them, plus deciding which one a drag is aiming at, which one
-- (if any) is showing its cards, and what happens when the finger lets go.

Shelves = class()

function Shelves:init(cardTable)
  self.cardTable = cardTable
  self.all = {}
  for _, edge in ipairs({"bottom", "top", "left", "right"}) do
    table.insert(self.all, Shelf(edge, cardTable))
  end
  self.aimed = nil
  self.shownShelf = nil
end

function Shelves:revealDistance()
  return self.cardTable:cardHeight() * 0.9
end

function Shelves:aimAt(body)
  local target, shortest = nil, self:revealDistance()
  if body then
    for _, shelf in ipairs(self.all) do
      local distance = shelf:distanceFrom(body.position)
      if distance < shortest then target, shortest = shelf, distance end
    end
  end
  for _, shelf in ipairs(self.all) do
    shelf.isAimed = (shelf == target)
  end
  self.aimed = target
end

function Shelves:clearAim()
  self.aimed = nil
  for _, shelf in ipairs(self.all) do shelf.isAimed = false end
end

-- returns true if the drop was taken by a shelf. atPosition, if given, is
-- where the finger let go, so the cards land where they were dropped.
function Shelves:dropIfAimed(body, atPosition)
  if not self.aimed or not body then return false end
  local shelf = self.aimed
  self.aimed = nil
  shelf.isAimed = false
  local cards = self.cardTable:stackContaining(body)
  if #cards == 0 then return false end
  for _, card in ipairs(cards) do
    self.cardTable.arranger:cancel(card.body)
  end
  local along = nil
  if atPosition then along = shelf:clampAlong(shelf:alongOf(atPosition)) end
  shelf:accept(cards, along)
  return true
end

function Shelves:release(body)
  if not body.shelf then return false end
  body.shelf:release(body.owner)
  return true
end

-- the shelf a touch lands on, if any
function Shelves:shelfAt(touch)
  for _, shelf in ipairs(self.all) do
    if not shelf:isEmpty() and shelf:containsTouch(touch) then return shelf end
  end
  return nil
end

-- reveal this shelf's cards, hiding whichever shelf was showing before
function Shelves:show(shelf)
  if self.shownShelf == shelf then return end
  if self.shownShelf then self.shownShelf:setFaceUp(false) end
  self.shownShelf = shelf
  shelf:setFaceUp(true)
end

function Shelves:hideShown()
  if not self.shownShelf then return end
  self.shownShelf:setFaceUp(false)
  self.shownShelf = nil
end

function Shelves:update()
  for _, shelf in ipairs(self.all) do shelf:update() end
end

function Shelves:draw()
  for _, shelf in ipairs(self.all) do shelf:draw() end
end
