Player = class()

Player.GROUP = 1<<11

Player.WALK_SPEED = 7.2
Player.WALK_FORCE = 55
Player.FLY_SPEED  = 8
Player.FLY_FORCE  = 120

Player.SWIM_SPEED = 3.0
Player.SWIM_FORCE = 26
Player.SWIM_SINK  = 1.4
Player.SWIM_RISE  = 3.0

Player.BOUNCE = { Goo = 0.8 }
Player.BOUNCE_MIN = 1.5
Player.BOUNCE_MAX = 18

-- Auto-step: hops the player up a ledge exactly one block
-- high, the way Minecraft does, so a wall you're allowed to
-- climb doesn't stop you dead. Two short horizontal probes:
-- one near the feet, one a block up. Blocked low + clear high
-- means "step", not "wall".
Player.STEP_LOW_Y = -0.4
Player.STEP_HIGH_Y = 0.7
Player.STEP_RADIUS = 0.3
Player.STEP_REACH = 0.55

function Player:init(entity, camera, x, y, z)
  assert(touches, "Please include Touches project as a dependency")
  assert(ui, "Please include UI project as a dependency")
  assert(FirstPersonViewer, "Please include Cameras project as a dependency")
  assert(BlockInventory, "Please include InventoryHUD project as a dependency")
  assert(blocks, "Please include Block Library project as a dependency")
  assert(VoxelToucher, "Please include Block Library project as a dependency")
  assert(allBlocks, "Blocks arent loaded, please add 'allBlocks = blocks()' to setup()")
  
  self.entity = entity
  
  self.camera = camera
  self.camera.ortho = false
  self.camera.fieldOfView = 70
  
  self.speed = Player.WALK_SPEED
  self.maxForce = Player.WALK_FORCE
  self.jumpForce = 5.5
  
  self.cart = nil
  
  local greyGlass = BlockInventory.tintedTheme
  {
    panelFill = color(101, 186),
    
    cellFill = color(155, 180),
    cellSelectedFill = color(255, 0, 199, 29),
    cellHighlightFill = color(0, 255, 23, 200),
    
    arrowFill = color(132, 138, 194, 180),
    arrowHighlightFill = color(220,200,255,200),
    
    titleColor = color(243, 240, 240),
    labelColor = color(230)
  }
  
  self.inventory = BlockInventory(8, 8, function(inv, slot)
    if slot.block == nil then return end
    
    local hotSlot = self.hotbar:getSelected()
    if hotSlot then
      self.hotbar:setBlock(hotSlot.index, slot.block)
    end
  end,
  
  {
    paged = true,
    theme = greyGlass
  })
  
  for i,v in ipairs(placeableBlocks(allBlocks)) do
    self.inventory:addBlock(v)
  end
  
  self.hotbar = BlockInventory(1, 8, function(inventory, slot)
    inventory:setSelected(slot)
    self.slot = slot
    sound(SOUND_PICKUP, 41674)
  end,
  {
    names = false,
    theme = greyGlass,
    decorate = function(inv)
      local lastSlot = inv:getSlot(inv:slotCount())
      lastSlot.button.onPressed = function(b)
        sound(SOUND_PICKUP, 41674)
        self:toggleInventory()
      end
      lastSlot.button.label.text = "INV"
    end
  })
  self.hotbar:dockBottom()
  
  local openY = self.inventory:openAnchorY()
  
  self.hotbar:load("Project:Hotbar", function(name)
    return scene.voxels.blocks:get(name)
  end)
  self.slot = self.hotbar:getSelected()
  
  self.joystick = Joystick()
  self.joystick.onDoubleTap = function()
    self:setFlying(not self.flying)
  end
  
  self.viewer = self.camera.entity:add(FirstPersonViewer)
  self.viewer.rx = 45
  self.viewer.ry = -45
  
  self.entity.position = vec3(x,y,z)
  self.camera.entity.parent = self.entity
  self.camera.entity.position = vec3(0,0.85,0)
  
  self.voxelToucher = self.camera.entity:add(VoxelToucher, self.camera,
  function()
    return self.slot and self.slot.block
  end)
  
  self.voxelToucher.onTap = function(tx, ty)
    if self.cart then self.leaveRequest = true return true end
    local c = minecartAtScreen(self.camera, tx, ty)
    if c then self.boardRequest = c return true end
    return false
  end
  
  self:addPhysics()
end

function Player:setFlying(flying)
  self.flying = flying
  if self.joystick then self.joystick.flying = flying end
  
  if flying then
    self.speed = Player.FLY_SPEED
    self.maxForce = Player.FLY_FORCE
    scene.physics.gravity = vec3(0, 0, 0)
    if self.rb then self.rb.linearDamping = 0.9 end
  else
    self.speed = Player.WALK_SPEED
    self.maxForce = Player.WALK_FORCE
    scene.physics.gravity = vec3(0, -14.8, 0)
  end
end

function Player:jump()
  if self.grounded then
    local v = self.rb.linearVelocity
    v.y = self.jumpForce
    self.rb.linearVelocity = v
  end
end

function Player:toggleInventory()
  local y = 1.0
  if not self.isInventoryOpen then
    y = self.inventory:openAnchorY()
  end
  self.isInventoryOpen = not self.isInventoryOpen
  tween(0.6, self.inventory.panel.anchor, {y = y}, tween.easing.cubicInOut)
end

function Player:relayout()
  self.inventory:relayout()
  self.hotbar:relayout()
  self.joystick:layout()
end

function Player:addPhysics()
  self.rb = self.entity:add(craft.rigidbody, DYNAMIC, 1)
  self.rb.angularFactor = vec3(0,0,0)
  self.rb.sleepingAllowed = false
  self.rb.friction = 0.5
  self.rb.group = Player.GROUP
  self.entity:add(craft.shape.capsule, 0.5, 1.0)
  
  self:setFlying(self.flying == true)
end

function Player:cartTick()
  if self.boardRequest then
    local c = self.boardRequest
    self.boardRequest = nil
    self:rideCart(c)
  end
  
  if self.leaveRequest then
    self.leaveRequest = nil
    self:leaveCart()
  end
  
  if self.cart then
    if self.rb then
      self.rb.linearVelocity = vec3(0, 0, 0)
    end
    local p = self.cart.entity.position
    self.entity.position = vec3(p.x, p.y + Minecart.EYE, p.z)
  end
end

function Player:rideCart(cart)
  if self.cart then return end
  
  cart:aim(self.camera.entity.forward)
  
  if self.rb then
    self.savedMask = self.rb.mask
    self.rb.mask = 0
    self.rb.linearVelocity = vec3(0, 0, 0)
  end
  
  self.cart = cart
  cart:setRider(self.entity)
  
  sound(SOUND_PICKUP, 41674)
end

function Player:leaveCart()
  if not self.cart then return end
  
  local p = self.cart.entity.position
  self.cart:setRider(nil)
  self.cart = nil
  
  local out = self.camera.entity.right
  out.y = 0
  if out:len() > 0 then out = out:normalize() end
  
  self.entity.position = p + out * 1.2 + vec3(0, 1.0, 0)
  
  if self.rb then
    self.rb.linearVelocity = vec3(0, 0, 0)
    if self.savedMask then
      self.rb.mask = self.savedMask
    else
      self.rb.mask = ~0
    end
  end
  self.savedMask = nil
  
  sound(SOUND_PICKUP, 41674)
end

-- The capsule is radius 0.5, height 1.0, ground check
-- sphereCasts about half a unit below origin, so the feet
-- sit roughly one unit down.
function Player:blockUnderFoot()
  local p = self.entity.position
  local y = math.floor(p.y - 1.05)
  if y < 0 then return nil end
  
  local name = nil
  pcall(function()
    name = scene.voxels:get(math.floor(p.x), y, math.floor(p.z), BLOCK_NAME)
  end)
  return name
end

-- Tracks the fastest downward velocity seen while airborne,
-- since whether update runs before or after the physics step
-- isn't something this code should have to know.
function Player:bounceCheck()
  if self.flying then
    self.fallSpeed = 0
    return
  end
  
  local vy = self.rb.linearVelocity.y
  
  if not self.grounded then
    if vy < (self.fallSpeed or 0) then
      self.fallSpeed = vy
    end
    return
  end
  
  local drop = -(self.fallSpeed or 0)
  self.fallSpeed = 0
  if drop < Player.BOUNCE_MIN then return end
  
  local name = self:blockUnderFoot()
  local k = name and Player.BOUNCE[name]
  if not k then return end
  
  local v = self.rb.linearVelocity
  v.y = math.min(drop * k, Player.BOUNCE_MAX)
  self.rb.linearVelocity = v
  
  self.grounded = false
end

function Player:blockAtEye()
  local p = self.entity.position
  local name = nil
  pcall(function()
    name = scene.voxels:get(math.floor(p.x),
    math.floor(p.y + 0.85), math.floor(p.z), BLOCK_NAME)
  end)
  return name
end

function Player:blockAtBody()
  local p = self.entity.position
  local name = nil
  pcall(function()
    name = scene.voxels:get(math.floor(p.x),
    math.floor(p.y), math.floor(p.z), BLOCK_NAME)
  end)
  return name
end

-- Head and body are separate: the underwater tint keys off
-- the head only, the movement mode off either, so wading
-- slows you down before it drowns the camera.
function Player:updateWater()
  self.headUnder = (self:blockAtEye() == "Water")
  self.inWater = self.headUnder or (self:blockAtBody() == "Water")
  
  local mode = "walk"
  if self.flying then
    mode = "fly"
  elseif self.inWater then
    mode = "swim"
  end
  if self.joystick then self.joystick.mode = mode end
  self.moveMode = mode
end

-- Only fires while grounded and actually moving. A near
-- vertical face close ahead, with clear space one block
-- above it, reads as a step; anything else (a slope, an
-- overhang, open air) does not.
function Player:checkAutoStep(horizDir)
  if not self.grounded then return end
  if horizDir:len() <= 0 then return end
  
  local p = self.entity.position
  local lowFrom = p + vec3(0, Player.STEP_LOW_Y, 0)
  local highFrom = p + vec3(0, Player.STEP_HIGH_Y, 0)
  
  local low = scene.physics:sphereCast(lowFrom, horizDir,
  Player.STEP_RADIUS, Player.STEP_REACH, ~0, ~Player.GROUP)
  if not low then return end
  if math.abs(low.normal.y) > 0.3 then return end
  
  local high = scene.physics:sphereCast(highFrom, horizDir,
  Player.STEP_RADIUS, Player.STEP_REACH, ~0, ~Player.GROUP)
  if high then return end
  
  self:jump()
end

function Player:update()
  if self.cart then return end
  
  self.grounded = false
  self:updateWater()
  
  local swim = (self.moveMode == "swim")
  local speed, force = self.speed, self.maxForce
  if swim then
    speed = Player.SWIM_SPEED
    force = Player.SWIM_FORCE
  end
  
  local moveDir = vec3(0, 0, 0)
  
  local jx, jy = self.joystick:ratio()
  if jx ~= 0 or jy ~= 0 then
    moveDir.z = jy
    moveDir.x = -jx
  end
  
  if self.flying then
    if self.joystick.rising or self.joystick:isLongPressRising() then
      moveDir.y = moveDir.y + 1
    end
    if self.joystick.descending then moveDir.y = moveDir.y - 1 end
  elseif swim then
    if self.joystick.rising then moveDir.y = moveDir.y + 1 end
    if self.joystick.descending then moveDir.y = moveDir.y - 1 end
  end
  
  local hit1 = scene.physics:sphereCast(self.entity.position,
  vec3(0, -1, 0), 0.52, 0.48, ~0, ~Player.GROUP)
  if hit1 and hit1.normal.y > 0.5 then
    self.grounded = true
  end
  
  self:bounceCheck()
  
  if not self.flying and not swim and self.joystick.jumping then
    self:jump()
  end
  
  if not self.flying and not swim and moveDir:len() > 0 then
    local horiz = self.camera.entity.forward * moveDir.z
    + self.camera.entity.right * moveDir.x
    horiz.y = 0
    if horiz:len() > 0 then
      self:checkAutoStep(horiz:normalize())
    end
  end
  
  if not self.flying then
    if self.grounded then
      self.rb.friction = 0.95
    else
      self.rb.friction = 0
    end
    self.rb.linearDamping = swim and 0.6 or 0.0
  end
  
  if moveDir:len() > 0 then
    moveDir = moveDir:normalize()
    
    local forward = self.camera.entity.forward * moveDir.z
    local right = self.camera.entity.right * moveDir.x
    local up = vec3(0, 1, 0) * moveDir.y
    
    local finalDir = forward + right + up
    
    if not self.flying and not swim then
      finalDir.y = 0
    end
    
    if finalDir:len() > 0 then
      finalDir = finalDir:normalize()
    end
    
    self.rb:applyForce(finalDir * force)
  end
  
  local v = self.rb.linearVelocity
  
  if swim then
    if self.joystick.rising then
      if v.y < Player.SWIM_RISE then v.y = Player.SWIM_RISE end
    elseif v.y < -Player.SWIM_SINK then
      v.y = -Player.SWIM_SINK
    end
    self.rb.linearVelocity = v
  end
  
  local vy = self.rb.linearVelocity.y
  local flat = self.rb.linearVelocity
  if not self.flying then flat.y = 0 end
  
  if flat:len() > speed then
    flat = flat:normalize() * speed
    if not self.flying then flat.y = vy end
    self.rb.linearVelocity = flat
  end
end

-- Tiled water texture at low alpha with a slow drift, plus a
-- blue wash. No mesh, no shader -- if it still reads flat,
-- that's the point at which a render-target version earns
-- its cost.
function Player:drawUnderwater()
  if not self.headUnder then return end
  
  pushStyle()
  resetMatrix()
  noStroke()
  
  if not self.waterTex then
    pcall(function()
      self.waterTex = readImage(packPrefix.."Water")
    end)
    if not self.waterTex then self.waterTex = false end
  end
  
  if self.waterTex then
    spriteMode(CORNER)
    tint(255, 255, 255, 40)
    local tile = 128
    local wob = math.sin(ElapsedTime * 1.6) * 10
    local wob2 = math.cos(ElapsedTime * 1.1) * 10
    for x = -tile, WIDTH, tile do
      for y = -tile, HEIGHT, tile do
        sprite(self.waterTex, x + wob, y + wob2, tile, tile)
      end
    end
    noTint()
  end
  
  fill(30, 90, 170, 90)
  rect(0, 0, WIDTH, HEIGHT)
  popStyle()
end

function Player:draw()
  self:drawUnderwater()
  self.voxelToucher:draw()
  self.joystick:draw()
  self.inventory:draw()
  self.hotbar:draw()
end
