const = require("const")
graphics = require("graphics")
bodies = require("bodies")
misc = require("misc")

local snake = {}

snake.LENGTH = 5

snake.idleStraightSpriteSheet = graphics.SpriteSheet:new("snakeIdleStraight.png", 5)
snake.moveStraightSpriteSheets = graphics.loadMulti("snake", 6, "MoveStraight.png", 7)

snake.idleClockwiseSpriteSheet = graphics.SpriteSheet:new("snakeIdleClockwise.png", 3)
snake.moveClockwiseSpriteSheets = graphics.loadMulti("snake", 4, "MoveClockwise.png", 7)

snake.idleCounterClockwiseSpriteSheet = graphics.SpriteSheet:new("snakeIdleCounterClockwise.png", 3)
snake.moveCounterClockwiseSpriteSheets = graphics.loadMulti("snake", 4, "MoveCounterClockwise.png", 7)

snake.tailWaggle1SpriteSheet = graphics.SpriteSheet:new("snakeTailWaggle1.png", 8)
snake.tailWaggle2SpriteSheet = graphics.SpriteSheet:new("snakeTailWaggle2.png", 5)

snake.attackStraightSpriteSheet = graphics.SpriteSheet:new("snakeAttackStraight.png", 6)
snake.attackClockwiseSpriteSheet = graphics.SpriteSheet:new("snakeAttackClockwise.png", 7)
snake.attackCounterClockwiseSpriteSheet = graphics.SpriteSheet:new("snakeAttackCounterClockwise.png", 7)

snake.idleSound = sound.SoundSet:new("snakeHiss", 6, ".ogg", 0.3)
snake.attackSound = sound.SoundSet:new("snakeAttack", 4, ".ogg", 0.3)

snake.Snake = {}

--- Constructor.  Makes a snake enemy.
-- The snake enemy consists of 5 segments.
-- The enemy always waits two turns, then moves closer to the player.
function snake.Snake:new(spaceList, directionList)
  
  local newObj = {
    moveTimer = math.random(0, 2),
    
    headAnimation = nil,
    attackStraightAnim = graphics.Animation:new(snake.attackStraightSpriteSheet),
    attackClockwiseAnim = graphics.Animation:new(snake.attackClockwiseSpriteSheet),
    attackCounterClockwiseAnim = graphics.Animation:new(snake.attackCounterClockwiseSpriteSheet),
    
    tailAnimation = nil,
    idleAnim1 = graphics.Animation:new(snake.tailWaggle1SpriteSheet),
    idleAnim2 = graphics.Animation:new(snake.tailWaggle2SpriteSheet),
    
    moveStraightAnims = graphics.multiAnim(snake.moveStraightSpriteSheets),
    moveClockwiseAnims = graphics.multiAnim(snake.moveClockwiseSpriteSheets),
    moveCounterClockwiseAnims = graphics.multiAnim(snake.moveCounterClockwiseSpriteSheets),
    
    bodyList = {},
    
    -- Stores direction of tail before a movement, so that during the movement it can be drawn properly
    previousTailDirection = nil,
    
    soundTimer = math.random(1, 1200),
  }
  
  if newObj.moveTimer == 1 then
    newObj.tailAnimation = newObj.idleAnim1
  elseif newObj.moveTimer == 2 then
    newObj.tailAnimation = newObj.idleAnim2
  end
  
  newObj.idleAnim1:setFrameLength(6)
  newObj.idleAnim2:setFrameLength(4)
  
  newObj.attackStraightAnim:setFrameLength(3)
  newObj.attackClockwiseAnim:setFrameLength(3)
  newObj.attackCounterClockwiseAnim:setFrameLength(3)
  
  graphics.setMultiAnimFrameLength(newObj.moveStraightAnims, 3)
  graphics.setMultiAnimFrameLength(newObj.moveClockwiseAnims, 3)
  graphics.setMultiAnimFrameLength(newObj.moveCounterClockwiseAnims, 3)
  
  -- Creates all the bodies
  for i = 1, snake.LENGTH do
    newObj.bodyList[i] = bodies.WarpBody:new(spaceList[i])
    newObj.bodyList[i]:addFleas(1)
  end

  -- Determines the initial direction of all the parts
  for i = 1, snake.LENGTH do
    newObj.bodyList[i].moveDirection = directionList[i]
  end
  
  -- Determines the initial direction of all the parts
  -- Old, autogenerated directions sometimes generate u-turns, which are invalid
  --[[
  local previousBody
  local currentBody

  for i = 2, snake.LENGTH do
    previousBody = newObj.bodyList[i - 1]
    currentBody = newObj.bodyList[i]

    previousBody.moveDirection = currentBody.space:directionOf(previousBody.space)
  end
  newObj.bodyList[snake.LENGTH].moveDirection = newObj.bodyList[snake.LENGTH - 1].moveDirection
  ]]
  
  self.__index = self
  return setmetatable(newObj, self)
end


--- Constructor.  Makes a snake enemy, with random spaces spreading out from a starting location.
-- Returns nil if a valid snake could not be generated.
function snake.Snake:newRandom(startSpace)
  if startSpace:isOccupied() then
    return nil
  end
  
  local spaceList
  
  local checkSpace
  local isValidSpace = false
  
  local fullRerolls = 0
  local snakeIsValid = false
  local spaceRerolls
  local spaceFound = false
  
  local adjacentLength
  
  local spaceOccupied
  
  local checkDirection
  local validDirections = {}
  local invalidDirection
  
  local directionList = {}
  
  -- Loops until a valid snake is returned
  while true do
    
    -- If 25 full-snake rerolls occur, then a valid snake could not be generated.
    if fullRerolls > 25 then
      print("Could not generate a snake here!")
      return nil
    else
      -- The snake is valid, until proven otherwise
      snakeIsValid = true
      fullRerolls = fullRerolls + 1
      
      invalidDirection = nil
    end
    
    -- Generates all the body sections (except the first, since that's the startSpace)
    spaceList = {startSpace}
    for i = 2, snake.LENGTH do
      
      -- Determines all valid directions the snake should spawn in.
      -- On the first loop, all directions are valid.
      validDirections = {}
      for j = 1, #const.DIRECTIONS do
        if const.DIRECTIONS[j] ~= invalidDirection then
          validDirections[const.DIRECTIONS[j]] = true
        end
      end
      
      -- Loops until a valid space for the next part of the snake is found
      spaceRerolls = 0
      isValidSpace = false
      while not isValidSpace do
        
        -- If 25 rerolls occur, then the snake probably trapped itself during generation.
        if spaceRerolls > 25 then
          spaceFound = false
          break
        else
          -- The space is valid, until proven otherwise
          isValidSpace = true
          spaceFound = true
          spaceRerolls = spaceRerolls + 1
        end
        
        -- Randomly chooses one of the adjacent spaces
        checkDirection = misc.randomChoice(validDirections)
        checkSpace = misc.randomChoice(spaceList[i - 1].adjacent[checkDirection])
        
        -- If there are no adjacent spaces in that direction, randomChoice returns nil
        if not checkSpace then
          isValidSpace = false
          
        else
        
          -- Checks if anything is on the space
          if checkSpace:isOccupied() then
            isValidSpace = false
          end
          
          -- Checks if the space is the same as a previous space
          for j = 1, i - 1 do
            if checkSpace == spaceList[j] then
              isValidSpace = false
              break
            end
          end
          
        end
        
      end
      
      -- If one of the spaces could not be found, reroll the whole snake
      if not spaceFound then
        snakeIsValid = false
        break
        
      -- Otherwise, set this space as the next space in the list
      else
        spaceList[i] = checkSpace
        invalidDirection = misc.oppositeOf(checkDirection)
        directionList[i - 1] = invalidDirection
        
      end
      
    end
    
    -- If all the spaces were generated validly
    if snakeIsValid then
      
      -- Check that the snake didn't trap itself
      for space, _ in pairs(spaceList[1].adjacentList) do
        spaceOccupied = false
        
        for i = 1, snake.LENGTH do
          if space == spaceList[i] then
            spaceOccupied = true
          end
        end
        
        -- Return the snake if it has at least one space to move to
        if not spaceOccupied then
          directionList[snake.LENGTH] = directionList[snake.LENGTH - 1]
          return self:new(spaceList, directionList)
        end
        
      end
      
      -- If not, then reroll
    end
    
    -- Otherwise, go back to the start of the while loop and reroll the entire snake.
    
  end
end


--- Makes the snake move.
-- The head of the snake always moves one step closer to the player.
-- The rest of the parts follow the part before it.
function snake.Snake:move(player)
  local closestSpace
  local closestDirection
  
  local direction
  local previousSpace
  local nextSpace
  local previousDirection
  local nextDirection
  
  -- Finds the closest space that isn't moving backwards
  local head = self.bodyList[1]
  local restricted = misc.oppositeOf(head.moveDirection)
  closestSpace, closestDirection = head.space:closestAdjacentNotDirection(player, restricted)

  if closestSpace then
    
    self.previousTailDirection = self.bodyList[snake.LENGTH].moveDirection
    
    -- For each snake part, move to the next space.
    -- The next space for the head is the closest adjacent space to the player.
    -- The next space for all the other parts is the pre-movement space of the previous part.
    -- Note that the tail always takes the direction of the previous part.
    local body
    nextSpace = closestSpace
    nextDirection = self.bodyList[1].space:directionOfRestricted(nextSpace, restricted)
    for i = 1, snake.LENGTH do
      body = self.bodyList[i]
      
      previousSpace = body.space
      previousDirection = body.moveDirection

      body:moveTo(nextSpace, nextDirection)
      
      nextSpace = previousSpace
      nextDirection = previousDirection
    end
    
  -- If no valid space was found then
  else
    -- Play the "can't move" animation
  end
    
end


--- Returns whether the snake can move to a given space.
function snake.Snake:canMoveTo(space)
  local restrictedDirection = misc.oppositeOf(self.bodyList[1].moveDirection)
  for direction, spaceList in pairs(self.bodyList[1].space.adjacent) do
    
    if direction ~= restrictedDirection then
      -- print("Snake direction: ", direction, restrictedDirection, "ALLOWED")
      if spaceList[space] then
        return true
      end
      
    -- else
      -- print("Snake direction: ", direction, restrictedDirection, "NOT ALLOWED")
      
    end
    
  end
end


--- Makes the snake take a turn.
-- Snakes always wait two turns, then move closer to the player.
function snake.Snake:takeTurn(level, player)
  self.moveTimer = self.moveTimer + 1
  
  -- Updates tail waggle animations
  if self.moveTimer == 1 then
    self.idleAnim1:reset()
    self.tailAnimation = self.idleAnim1
    
  elseif self.moveTimer == 2 then
    self.idleAnim2:reset()
    self.tailAnimation = self.idleAnim2
    
  -- Makes the snake move
  elseif self.moveTimer == 3 then
    
    -- If the snake is beside the player, hurt them
    if self:canMoveTo(player.body.space) then
      player:hurt()
      
      -- Play the corresponding attack animation
      local headDirection = self.bodyList[1].moveDirection
      local playerDirection = self.bodyList[1].space:directionOf(player.body.space)
      if playerDirection == headDirection then
        self.headAnimation = self.attackStraightAnim
      elseif playerDirection == misc.clockwiseTo(headDirection) then
        self.headAnimation = self.attackClockwiseAnim
      elseif playerDirection == misc.counterClockwiseTo(headDirection) then
        self.headAnimation = self.attackCounterClockwiseAnim
      end
      
      snake.attackSound:playRandom()
      
    -- Otherwise, just move normally
    else
      self:move(player)
    end
    
    self.moveTimer = 0
  end
end


--- Draws one part of the snake's body, in its idle state.
function snake.Snake:drawIdle(bodyNum, gridXOffset, gridYOffset, scale, tileSize)
  local x
  local y
  local rotation
  
  local spriteSheet
  local spriteNum
  
  -- Head is always drawn straight
  if bodyNum == 1 then
    
    -- Draws the attacking animation
    if self.headAnimation then
      rotation = misc.rotationOf(self.bodyList[bodyNum].moveDirection)
      
      for colNum, col in pairs(self.bodyList[bodyNum].space.cells) do
        for rowNum, _ in pairs(col) do
          x = gridXOffset + ((colNum - 1) * tileSize)
          y = gridYOffset + ((rowNum - 1) * tileSize)
          
          self.headAnimation:draw(x, y, scale, rotation)
        end
      end
      
      return
    end
      
    spriteSheet = snake.idleStraightSpriteSheet
    spriteNum = bodyNum
    
  elseif bodyNum == snake.LENGTH then
    
    -- Draws tail wagging animation
    if self.moveTimer == 1 or self.moveTimer == 2 then
      rotation = misc.rotationOf(self.bodyList[bodyNum].moveDirection)
  
      for colNum, col in pairs(self.bodyList[bodyNum].space.cells) do
        for rowNum, _ in pairs(col) do
          x = gridXOffset + ((colNum - 1) * tileSize)
          y = gridYOffset + ((rowNum - 1) * tileSize)
          
          self.tailAnimation:draw(x, y, scale, rotation)
        end
      end
      
      return
    end
    
    -- Otherwise, sets up the tail to be drawn normally
    spriteSheet = snake.idleStraightSpriteSheet
    spriteNum = bodyNum
    
  else
    local currentDirection = self.bodyList[bodyNum].moveDirection
    local previousDirection = self.bodyList[bodyNum - 1].moveDirection
    
    -- If both body parts came from the same direction, then the body part is straight
    if previousDirection == currentDirection then
      spriteSheet = snake.idleStraightSpriteSheet
      spriteNum = bodyNum
      
    -- From right (currentDirection == left) going up (previousDirection == up)
    elseif previousDirection == misc.counterClockwiseTo(currentDirection) then
      spriteSheet = snake.idleCounterClockwiseSpriteSheet
      spriteNum = bodyNum - 1
      
    -- From left (currentDirection == right) going up (previousDirection == down)
    elseif previousDirection == misc.clockwiseTo(currentDirection) then
      spriteSheet = snake.idleClockwiseSpriteSheet
      spriteNum = bodyNum - 1
    
    -- Default to straight if something invalid happens
    else
      spriteSheet = snake.idleStraightSpriteSheet
      print("This shouldn't happen?  Current direction and previous direction are opposite.")
    end
  end
  
  rotation = misc.rotationOf(self.bodyList[bodyNum].moveDirection)
  
  for colNum, col in pairs(self.bodyList[bodyNum].space.cells) do
    for rowNum, _ in pairs(col) do
      x = gridXOffset + ((colNum - 1) * tileSize)
      y = gridYOffset + ((rowNum - 1) * tileSize)
      
      spriteSheet:draw(spriteNum, x, y, scale, rotation)
    end
  end
end


-- Draws one part of the snake's body, in its moving state.
-- This splits the snake into six parts - the first part is the head's new space,
-- and the last part is the tail's old space
function snake.Snake:drawMoving(bodyNum, gridXOffset, gridYOffset, scale, tileSize)
  local x
  local y
  local rotation
  
  local animation
  
  -- New head and old tail are always drawn straight
  if bodyNum == 1 or bodyNum == snake.LENGTH + 1 then
    animation = self.moveStraightAnims[bodyNum]
    
  else
    local currentDirection = self.bodyList[bodyNum].moveDirection
    local previousDirection = self.bodyList[bodyNum - 1].moveDirection
    
    -- If both body parts are moving in the same direction, then the body part is straight
    if previousDirection == currentDirection then
      animation = self.moveStraightAnims[bodyNum]
      
    -- From right (currentDirection == left) going up (previousDirection == up)
    elseif previousDirection == misc.counterClockwiseTo(currentDirection) then
      animation = self.moveCounterClockwiseAnims[bodyNum - 1]
      
    -- From left (currentDirection == right) going up (previousDirection == down)
    elseif previousDirection == misc.clockwiseTo(currentDirection) then
      animation = self.moveClockwiseAnims[bodyNum - 1]
      
    -- Default to the moving straight animation in all invalid situations
    else
      animation = self.moveStraightAnims[bodyNum]
      
    end
    
  end
  
  local cells
  if bodyNum == snake.LENGTH + 1 then
    cells = self.bodyList[snake.LENGTH].previousSpace.cells
    rotation = misc.rotationOf(self.previousTailDirection)
  else
    cells = self.bodyList[bodyNum].space.cells
    rotation = misc.rotationOf(self.bodyList[bodyNum].moveDirection)
  end
  
  for colNum, col in pairs(cells) do
    for rowNum, _ in pairs(col) do
      x = gridXOffset + ((colNum - 1) * tileSize)
      y = gridYOffset + ((rowNum - 1) * tileSize)
        
      animation:draw(x, y, scale, rotation)
    end
  end
end
    

--- Draws the snake.
function snake.Snake:draw(gridXOffset, gridYOffset, scale, tileSize)
  local rotation
  local color
  
  if self.bodyList[1].moving then

    for i = 1, snake.LENGTH + 1 do
      self:drawMoving(i, gridXOffset, gridYOffset, scale, tileSize)
    end
    
  else
    
    for i = 1, snake.LENGTH do
      self:drawIdle(i, gridXOffset, gridYOffset, scale, tileSize)
    end
    
  end
  
  for i = 1, snake.LENGTH do
    self.bodyList[i]:drawBugs(gridXOffset, gridYOffset, scale, tileSize)
  end
  
end


--- Updates the snake's animation.  Should be called every frame.
function snake.Snake:updateAnimation()
  if self.bodyList[1].moving then
    graphics.updateMultiAnim(self.moveStraightAnims)
    graphics.updateMultiAnim(self.moveClockwiseAnims)
    graphics.updateMultiAnim(self.moveCounterClockwiseAnims)
  
  -- Tail waggle animations
  elseif self.moveTimer == 1 or self.moveTimer == 2 then
    self.tailAnimation:update()
    
    if self.tailAnimation.isDone then
      self.tailAnimation:reset()
    end
  end
  
  if self.moveStraightAnims[1].isDone then
    for i = 1, snake.LENGTH do
      self.bodyList[i].moving = false
    end
    
    graphics.resetMultiAnim(self.moveStraightAnims)
    graphics.resetMultiAnim(self.moveClockwiseAnims)
    graphics.resetMultiAnim(self.moveCounterClockwiseAnims)
    
    -- Update's the tail's direction, as it faces a different direction during
    -- the animation
    self.bodyList[snake.LENGTH].moveDirection = self.bodyList[snake.LENGTH - 1].moveDirection
  end
  
  if self.headAnimation then
    self.headAnimation:update()
    
    if self.headAnimation.isDone then
      self.headAnimation:reset()
      self.headAnimation = nil
      
    end
  end
  
  for i = 1, snake.LENGTH do
    self.bodyList[i]:updateBugs()
  end
  
  -- Updates the sound timer
  self.soundTimer = self.soundTimer - 1
  if self.soundTimer <= 0 then
    self.soundTimer = math.random(600, 1200)
    snake.idleSound:playRandom()
  end
end


return snake