--------------------------------------------------------------------------------
function init()
  self.timers = createTimers()

  self.state = stateMachine.create({
    "smashAttack",
    "riseState",
    "followState",
    "rangedAttack",
    "followState",
    "meleeAttack",
    "followState",
  })

  self.state.enteringState = function(stateName)
    self.state.moveStateToEnd(stateName)
  end

  self.state.leavingState = function(stateName)
    self.hoverTimer = nil
  end

  entity.setAggressive(true)

  -- Setup blinking
  for i = 1, 3 do
    local animationStateName = "eye" .. i
    local blinkDelay = entity.randomizeParameterRange("blinkTimeRange")
    self.timers.start(blinkDelay, function()
      local animation = entity.animationState(animationStateName)
      if animation == "open" then
        entity.setAnimationState(animationStateName, "blink")
      end
      return blinkDelay
    end)
  end

  self.state.pickState({ requestedState = "entrance" })
end

--------------------------------------------------------------------------------
function update(dt)
  if not self.state.update(dt) then
    mcontroller.controlFly({ 0, 0 })
  end

  self.timers.tick(dt)
end

--------------------------------------------------------------------------------
function moveTo(destination, maxSpeed)
  moveBy(world.distance(destination, mcontroller.position()), maxSpeed)
end

--------------------------------------------------------------------------------
function moveBy(delta, maxSpeed)
  if maxSpeed ~= nil then
    local speed = world.magnitude(delta)
    delta = vec2.mul(vec2.norm(delta), math.min(speed, maxSpeed))
  end

  mcontroller.setVelocity(vec2.div(delta, script.updateDt()))
end

--------------------------------------------------------------------------------
function hoverOffset(dt)
  if self.hoverTimer == nil then
    self.hoverTimer = 0
  else
    self.hoverTimer = self.hoverTimer + dt
  end

  local angle = 2.0 * math.pi * entity.configParameter("hoverFrequency") * self.hoverTimer
  return entity.configParameter("hoverAmplitude") * math.sin(angle)
end

--------------------------------------------------------------------------------
function findTargets(radius)
  local position = mcontroller.position()
  local targetIds = world.entityQuery(position, radius, {
    -- validTargetOf = entity.id(), -- deprecated, but so is this whole behavior file
    includedTypes = {"creature"},
    order = "nearest"
  })

  -- Prefer an existing target as long as it stays within the search radius
  if self.targetId ~= nil then
    local targetPosition = world.entityPosition(self.targetId)
    if targetPosition ~= nil then
      local distance = world.magnitude(targetPosition, position)
      if distance < radius then
        table.insert(targetIds, self.targetId)
      else
        self.targetId = nil
      end
    else
      self.targetId = nil
    end
  end

  return targetIds
end

--------------------------------------------------------------------------------
function closestCraterOffset(targetPosition)
  local minDistance, bestOffset, bestOffsetIndex = nil, nil, nil

  for index, offset in ipairs(entity.configParameter("craterOffsets")) do
    local distance = world.magnitude(entity.toAbsolutePosition(offset), targetPosition)
    if minDistance == nil or distance < minDistance then
      minDistance = distance
      bestOffset = offset
      bestOffsetIndex = index
    end
  end

  return bestOffset, bestOffsetIndex
end

--------------------------------------------------------------------------------
riseState = {
  riseSpeedFraction = 0.15,
  riseHeight = 18,
  maxRiseTime = 4,
  pauseTime = 2,
}

function riseState.enterWith(params)
  if params.requestedState ~= "rise" then return nil end
  return { initialPosition = mcontroller.position(), timer = 0 }
end

function riseState.update(dt, stateData)
  -- Pause for a bit after rising up
  if stateData.pauseTimer ~= nil then
    if stateData.hoverPosition == nil then
      stateData.hoverPosition = mcontroller.position()
    end
    moveTo(vec2.add(stateData.hoverPosition, { 0, hoverOffset(dt) }))

    if entity.animationState("eye1") == "closed" then
      entity.setAnimationState("eye1", "open")
      entity.setAnimationState("eye2", "open")
      entity.setAnimationState("eye3", "open")
    end

    stateData.pauseTimer = stateData.pauseTimer - dt
    return stateData.pauseTimer <= 0
  end

  local distance = world.magnitude(mcontroller.position(), stateData.initialPosition)
  stateData.timer = stateData.timer + dt
  if distance >= riseState.riseHeight or stateData.timer >= riseState.maxRiseTime then
    stateData.pauseTimer = riseState.pauseTime
  end

  moveBy({ 0, entity.flySpeed() * riseState.riseSpeedFraction })

  return false
end

--------------------------------------------------------------------------------
followState = {
  targetSearchRadius = 30,
  duration = 10,
  heightOffset = 15,
  trackTargetXInterval = 0.25,
  trackTargetYInterval = 1,
  moveSpeedFraction = 0.1,
}

followState.enter = function()
  local targetIds = findTargets(followState.targetSearchRadius)
  if #targetIds > 0 then
    return {
      timer = followState.duration,
      targetId = targetIds[1],
      targetPosition = world.entityPosition(targetIds[1]),
      trackTargetXTimer = followState.trackTargetXInterval,
      trackTargetYTimer = followState.trackTargetYInterval,
    }
  end

  return nil
end

followState.update = function(dt, stateData)
  local targetPosition = world.entityPosition(stateData.targetId)
  if targetPosition == nil then return true end

  stateData.targetPosition[1] = targetPosition[1]
  stateData.trackTargetXTimer = stateData.trackTargetYTimer - dt
  if stateData.trackTargetXTimer <= 0 then
    stateData.targetPosition[1] = targetPosition[1]
    stateData.trackTargetXTimer = followState.trackTargetXInterval
  end

  stateData.trackTargetYTimer = stateData.trackTargetYTimer - dt
  if stateData.trackTargetYTimer <= 0 then
    stateData.targetPosition[2] = targetPosition[2]
    stateData.trackTargetYTimer = followState.trackTargetYInterval
  end

  local followPosition = {
    stateData.targetPosition[1],
    stateData.targetPosition[2] + followState.heightOffset + hoverOffset(dt)
  }
  moveTo(followPosition, entity.flySpeed() * followState.moveSpeedFraction)

  stateData.timer = stateData.timer - dt
  return stateData.timer <= 0
end

--------------------------------------------------------------------------------
-- Rise up a bit (optional), then smash down in the target's vicinity
smashAttack = {
  entranceTargetSearchRadius = 100,
  targetSearchRadius = 50,

  riseSpeedFraction = 0.2,
  riseHeight = 22,
  riseMaxTime = 4,

  smashSpeedFraction = 0.5,
  smashMaxTime = 3,
  smashTargetOffset = { 0, -2 },

  cooldown = 1,

  explosionOffset = { 0, -6.25 },
}

function smashAttack.enter()
  local targetIds = findTargets(smashAttack.targetSearchRadius)
  if #targetIds == 0 then return nil end

  return {
    targetId = targetIds[1],
    targetPosition = world.entityPosition(targetIds[1]),
    riseTimer = smashAttack.riseMaxTime,
  }
end

function smashAttack.enterWith(params)
  -- Called on initial spawn, this is just a version of this state with some
  -- additional effects/options
  if params.requestedState ~= "entrance" then return nil end

  local direction = { 0, -1 }

  local targetIds = findTargets(smashAttack.entranceTargetSearchRadius)
  if #targetIds > 0 then
    direction = smashAttack.targetDirection(targetIds[1])
  end

  return { direction = direction }
end

function smashAttack.update(dt, stateData)
  -- Delay one update after crashing to allow the explosion to trigger
  if stateData.landed then
    self.state.pickState({ requestedState = "rise" })
    return true, smashAttack.cooldown
  end

  local position = mcontroller.position()

  if stateData.riseTimer ~= nil then
    -- Rising up
    if not world.entityExists(stateData.targetId) then return true, smashAttack.cooldown end
    local delta = world.distance(stateData.targetPosition, mcontroller.position())

    if delta[2] < -smashAttack.riseHeight or stateData.riseTimer <= 0 then
      stateData.direction = smashAttack.targetDirection(stateData.targetId)
      stateData.riseTimer = nil
    else
      moveBy({ 0, entity.flySpeed() * smashAttack.riseSpeedFraction })
      stateData.riseTimer = stateData.riseTimer - dt
    end
  else
    -- Smashing down
    local angle = vec2.angle(stateData.direction)
    if stateData.direction[1] < 0 then angle = math.pi - angle end
    entity.rotateGroup("flames", angle + math.pi / 2, true)
    entity.setAnimationState("flames", "idle")

    local delta = vec2.mul(stateData.direction, entity.flySpeed() * smashAttack.smashSpeedFraction)

    moveBy(vec2.mul(vec2.norm(delta), entity.flySpeed() * smashAttack.smashSpeedFraction))

    local bounds = entity.configParameter("metaBoundBox")
    bounds = {
      bounds[1] + position[1] + delta[1],
      bounds[2] + position[2] + delta[2],
      bounds[3] + position[1] + delta[1],
      bounds[4] + position[2] + delta[2]
    }
    moveBy(delta)

    if stateData.smashTimer == nil then
      stateData.smashTimer = smashAttack.smashMaxTime
    end
    stateData.smashTimer = stateData.smashTimer - dt
    if stateData.smashTimer <= 0 then
      self.state.pickState({ requestedState = "rise" })
      return true, smashAttack.cooldown
    end

    if world.rectTileCollision(bounds) then
      -- entity.setFireDirection(smashAttack.explosionOffset, { 0, 0 })
      -- entity.startFiring("crashexplosion")
      stateData.landed = true
    end
  end

  return false
end

function smashAttack.leavingState(stateData)
  -- entity.stopFiring()
  entity.setAnimationState("flames", "hidden")
end

function smashAttack.targetDirection(targetId)
  local targetPosition = vec2.add(world.entityPosition(targetId), smashAttack.smashTargetOffset)
  local toTarget = world.distance(targetPosition, mcontroller.position())
  return vec2.norm(toTarget)
end

--------------------------------------------------------------------------------
rangedAttack = {
  targetSearchRadius = 30,
  duration = 1,
  cooldown = 1,
}

function rangedAttack.enter()
  local targetIds = findTargets(rangedAttack.targetSearchRadius)
  if #targetIds == 0 then return nil end

  return {
    targetId = targetIds[1],
    targetPosition = world.entityPosition(targetIds[1]),
    timer = rangedAttack.duration,
  }
end

function rangedAttack.enteringState(stateData)
  local fireOffset = closestCraterOffset(stateData.targetPosition)
  -- entity.setFireDirection(fireOffset, vec2.norm(fireOffset))
  -- entity.startFiring("meteor")
end

function rangedAttack.update(dt, stateData)
  mcontroller.controlFly({ 0, 0 })

  stateData.timer = stateData.timer - dt
  return stateData.timer <= 0, rangedAttack.cooldown
end

function rangedAttack.leavingState(stateData)
  -- entity.stopFiring()
end

--------------------------------------------------------------------------------
meleeAttack = {
  targetSearchRadius = 30,
  targetOffset = { 0, 6 },
  moveSpeedFraction = 0.3,
  tentacleSearchRadius = 12,
  maxTime = 4,
  pauseTime = 2,
  cooldown = 1,
  minDistance = 7,

  projectileLifetime = 0.5,
}

function meleeAttack.enter()
  local targetIds = findTargets(meleeAttack.targetSearchRadius)
  if #targetIds == 0 then return nil end

  return {
    targetId = targetIds[1],
    targetPosition = vec2.add(world.entityPosition(targetIds[1]), meleeAttack.targetOffset),
    timer = meleeAttack.maxTime,
    tentacleProjectileEntityIds = { nil, nil, nil, nil, nil, nil, nil, nil }
  }
end

function meleeAttack.update(dt, stateData)
  local targetIds = findTargets(meleeAttack.tentacleSearchRadius)
  for _, targetId in pairs(targetIds) do
    local offset, index = closestCraterOffset(world.entityPosition(targetId))
    local animationStateName = "tentacle" .. index
    if entity.animationState(animationStateName) == "hidden" then
      entity.setAnimationState(animationStateName, "extend")
    end
  end

  local craterOffsets = entity.configParameter("craterOffsets")
  for i = 1, 8 do
    local animationStateName = "tentacle" .. i
    local animation = entity.animationState(animationStateName)
    if animation == "idle" then
      if stateData.tentacleProjectileEntityIds[i] == nil or not world.entityExists(stateData.tentacleProjectileEntityIds[i]) then
        stateData.tentacleProjectileEntityIds[i] = world.spawnProjectile("tentaclecomet" .. i, mcontroller.position(), entity.id(), craterOffsets[i], true, {
          speed = 0,
          power = 20,
          timeToLive = meleeAttack.projectileLifetime
        })
      end
    end
  end

  if stateData.pauseTimer ~= nil then
    if stateData.hoverPosition == nil then
      stateData.hoverPosition = mcontroller.position()
    end
    moveTo(vec2.add(stateData.hoverPosition, { 0, hoverOffset(dt) }))

    stateData.pauseTimer = stateData.pauseTimer - dt
    return stateData.pauseTimer <= 0, meleeAttack.cooldown
  end

  local distance = world.magnitude(stateData.targetPosition, mcontroller.position())
  if distance <= meleeAttack.minDistance or stateData.timer <= 0 then
    stateData.pauseTimer = meleeAttack.pauseTime
  else
    moveTo(stateData.targetPosition, entity.flySpeed() * meleeAttack.moveSpeedFraction)
  end

  return false
end

function meleeAttack.leavingState(stateData)
  for i = 1, 8 do
    local animationStateName = "tentacle" .. i
    if entity.animationState(animationStateName) ~= "hidden" then
      entity.setAnimationState(animationStateName, "retract")
    end
  end
end
