LD = require("design.LevelDesignLibrary")
timer = require("level.timer")
local player, thisObj, thisLevel, owner, playerLocation, myLocation, lastStartPoint, finalEndDestination
local raycastVectorUpOffset = engine.Vector.New(0, 0.2, 0)
local initialEndpoint
local concussionLocations = {}
local pathToPlayerLocations = {}
local currentPathLocation = 1
local pathAtEnd = false
local linePoints = {}
local drawLineCooldownTimer, playerTrackingCooldownTimer, preDetonationEffectTimer, detonationEffectTimer, detonationDamageTimer, objectAndFXCleanupTimer
local canStartNewLine = false
local canDrawNextLine = true
local canKillLine = false
local canTriggerDamageWave = true
local lineCompleted = false
local segmentCounter = 0
local segmentStraightThreshold = 3
local totalLineLength = 0
local filteredEndDestination, lineEffect, preDetonationLineEffect, detonationLineEffect
local angleOrientation = "neg"
local offsetType = "Positional"
local numStoredFrames = 10
local rawEndDestination
local newVec = engine.Vector.New(0, 0, 0)
local lastDirection = newVec
local lastLocation
local playerInfo = {
  elapsedTimeValues = {},
  playerTravelDistanceValues = {},
  playerDirectionVecValues = {}
}
local fxStage1 = "Witch20_Line_Stage1"
local fxStage2 = "Witch20_Line_Stage2"
local fxStage3 = "Witch20_Line_Stage3"
local slowPlayerConcussion = "CNC_WITCH20_LINE_SLOWING_CONCUSSION_PLAYER"
local damagePlayerConcussion = "CNC_WITCH20_LINE_DAMAGE_CONCUSSION_PLAYER"
local aiReactionConcussion = "CNC_WITCH20_LINE_REACTION_CONCUSSION_AI"
local lineDrawSpeedCD = 0.01
local endCondition_Time = 2.5
local endCondition_Length = 30
local objectAndFXCleanupCooldown = 2.5
local randomLengthFactor = {1.1, 1.5}
local min_max_AngleOffset = {0.1, 1.5}
local min_max_PositionOffset = {0.1, 0.3}
local staticEndpoint = false
local postDrawDamage = true
local min_leadDistanceThreshold = 8
local max_leadDistanceThreshold = 5
local preDetonationEffectCooldown = 0.25
local detonationTimerCooldown = 0.3
local damageDelaySec = 0.15
local slowConcussionTriggerDistance = 4
local concussionTriggerDistance = 2
local playerLeadTime = 0.75
local playerMovementRateCap = 5
local finalPositionLengthOffset = 6
local navmeshEdgeOffset = {1, 1.5}
local drawDuration = lineDrawSpeedCD * 20 + 1.5
function OnScriptLoaded(level, obj)
  player = game.Player.FindPlayer()
  thisObj = obj
  thisLevel = level
  if obj ~= nil then
    owner = obj:GetEffectObjectCreator()
  end
  if player ~= nil then
    myLocation = obj:GetWorldPosition()
    lastStartPoint = myLocation
    initialEndpoint = player:GetWorldPosition()
    lastLocation = initialEndpoint
    finalEndDestination = myLocation
    drawLineCooldownTimer = StartLevelTimer(lineDrawSpeedCD)
    drawLineCooldownTimer:Stop()
    preDetonationEffectTimer = StartLevelTimer(preDetonationEffectCooldown, TriggerPreDetonationFX)
    preDetonationEffectTimer:Stop()
    detonationEffectTimer = StartLevelTimer(detonationTimerCooldown, TriggerDetonationFX)
    detonationEffectTimer:Stop()
    detonationDamageTimer = StartLevelTimer(damageDelaySec, TriggerDamage)
    detonationDamageTimer:Stop()
    playerTrackingCooldownTimer = StartLevelTimer(endCondition_Time, StopLineDrawing)
    playerTrackingCooldownTimer:Restart()
    objectAndFXCleanupTimer = StartLevelTimer(objectAndFXCleanupCooldown, ObjectAndFXCleanup)
    objectAndFXCleanupTimer:Stop()
    if staticEndpoint == false then
      local distanceToPlayer = initialEndpoint:Distance(myLocation)
      if distanceToPlayer <= min_leadDistanceThreshold then
        staticEndpoint = true
      end
    end
    local findPathParams = {}
    local pathToPlayer
    if myLocation ~= nil and initialEndpoint ~= nil then
      findPathParams.StartLocation = game.NavMesh.ClosestLocation(myLocation)
      findPathParams.EndLocation = game.NavMesh.ClosestLocation(initialEndpoint, player)
    end
    if findPathParams.StartLocation ~= nil and findPathParams.EndLocation ~= nil then
      pathToPlayer = game.NavMesh.FindPath(findPathParams)
    end
    if pathToPlayer ~= nil then
      local distanceToPlayer = (obj:GetWorldPosition() - player:GetWorldPosition()):Length()
      initialEndpoint = LD.GetPointAlongLine(obj:GetWorldPosition(), initialEndpoint, distanceToPlayer + finalPositionLengthOffset)
      for i = 1, #pathToPlayer.PathArray - 2 do
        local dirVec1 = (pathToPlayer.PathArray[i] - pathToPlayer.PathArray[i + 1]):Normalized()
        local dirVec2 = (pathToPlayer.PathArray[i + 2] - pathToPlayer.PathArray[i + 1]):Normalized()
        local newVec = (dirVec1 + dirVec2):Normalized() * GetRandomFloat(navmeshEdgeOffset[1], navmeshEdgeOffset[2])
        local newPoint = pathToPlayer.PathArray[i + 1] - newVec
        newPoint = game.NavMesh.ClosestPoint(newPoint)
        table.insert(pathToPlayerLocations, newPoint)
      end
    else
      print("Witch00 Pestilence line cannot find path to player")
      canTriggerDamageWave = false
      lineCompleted = true
      playerTrackingCooldownTimer:Stop()
      thisObj:Destroy()
      return
    end
  else
    print("Witch00 spawned line, but there is no player. Destroying line")
    thisObj:Destroy()
    return
  end
  SoundInit()
end
function OnUpdate(level, obj)
  if not lineCompleted and obj ~= nil then
    myLocation = obj:GetWorldPosition()
    playerLocation = player:GetWorldPosition()
    StorePredictedLocationInfo(numStoredFrames)
    if totalLineLength >= endCondition_Length then
      canKillLine = true
      playerTrackingCooldownTimer:Stop()
    end
    if canKillLine or staticEndpoint and LD.GetXZDistanceBetweenTwoPoints(lastStartPoint, initialEndpoint) < 2 then
      EndLineAndStartDamageTimer()
    elseif staticEndpoint == false and filteredEndDestination ~= nil and LD.GetXZDistanceBetweenTwoPoints(lastStartPoint, filteredEndDestination) < 2 then
      EndLineAndStartDamageTimer()
    end
    if canDrawNextLine then
      DrawNextLineSegment()
    end
  else
    lineCompleted = true
    playerTrackingCooldownTimer:Stop()
  end
  UpdateLineGOPosition()
end
function EndLineAndStartDamageTimer()
  canKillLine = false
  canDrawNextLine = false
  if canTriggerDamageWave then
    lineCompleted = true
    if thisObj ~= nil and owner ~= nil then
      for i = 1, #concussionLocations do
        local distanceToPlayer = (concussionLocations[i] - player:GetWorldPosition()):Length()
        if distanceToPlayer <= slowConcussionTriggerDistance then
          local concussionParams = {
            Tweak = slowPlayerConcussion,
            WorldLocation = concussionLocations[i],
            Creature = owner,
            EnemyId = owner:GetID()
          }
          game.Combat.PlayConcussion(concussionParams)
        end
      end
    end
    preDetonationEffectTimer:Restart()
  end
end
function UpdateEndDestination()
  if staticEndpoint == false then
    local closestPoint = game.NavMesh.ClosestLocation(lastStartPoint)
    if closestPoint ~= nil then
      local unobstructed, obstructedPct = closestPoint:Raycast(player:GetWorldPosition())
      if unobstructed then
        pathAtEnd = true
      end
    end
    if pathAtEnd then
      if (myLocation - finalEndDestination):Length() < (playerLocation - myLocation):Length() then
        local predictedLocation = GetPredictedPlayerLocation(playerLeadTime)
        local distanceFromWitchToPredictedLocation = (myLocation - predictedLocation):Length()
        rawEndDestination = LD.GetPointAlongLine(myLocation, predictedLocation, distanceFromWitchToPredictedLocation + finalPositionLengthOffset)
        local distanceToNextPoint = (lastStartPoint - rawEndDestination):Length()
        filteredEndDestination = LD.GetPointAlongLine(lastStartPoint, rawEndDestination, distanceToNextPoint)
        local closestPoint = game.NavMesh.ClosestLocation(predictedLocation)
        if closestPoint ~= nil then
          local isWalkable, obstructedPct = closestPoint:Raycast(filteredEndDestination)
          if isWalkable == false then
            local tempDistance = (closestPoint.Position - filteredEndDestination):Length()
            filteredEndDestination = LD.GetPointAlongLine(closestPoint.Position, filteredEndDestination, tempDistance * obstructedPct)
          end
        end
      elseif filteredEndDestination == nil then
        filteredEndDestination = initialEndpoint
      end
      finalEndDestination = filteredEndDestination
    else
      finalEndDestination = GetNextPointAlongNavPath()
    end
  else
    finalEndDestination = initialEndpoint
  end
  local lengthOffsetVal = GetRandomFloat(randomLengthFactor[1], randomLengthFactor[2])
  totalLineLength = totalLineLength + lengthOffsetVal
  finalEndDestination = LD.GetPointAlongLine(lastStartPoint, finalEndDestination, lengthOffsetVal)
  if offsetType == "Rotational" then
    local randomAngle = math.random(min_max_AngleOffset[1], min_max_AngleOffset[2])
    randomAngle = randomAngle / 10
    if angleOrientation == "neg" then
      angleOrientation = "pos"
      finalEndDestination = finalEndDestination:RotateXZ(randomAngle)
    elseif angleOrientation == "pos" then
      angleOrientation = "neg"
      finalEndDestination = finalEndDestination:RotateXZ(-1 * randomAngle)
    end
  elseif offsetType == "Positional" then
    local xOffset = GetRandomFloat(min_max_PositionOffset[1], min_max_PositionOffset[2])
    local zOffset = GetRandomFloat(min_max_PositionOffset[1], min_max_PositionOffset[2])
    finalEndDestination = finalEndDestination + engine.Vector.New(GetRandomSign() * xOffset, 0, GetRandomSign() * zOffset)
  end
  finalEndDestination = game.NavMesh.ClosestPoint(finalEndDestination)
end
function DrawNextLineSegment()
  if drawLineCooldownTimer.running == false then
    if lineEffect == nil then
      lineEffect = game.FX.Spawn(fxStage1)
      PlayLineOneShot()
      lineEffect:AddPoint(finalEndDestination)
      table.insert(linePoints, finalEndDestination)
    end
    segmentCounter = segmentCounter + 1
    if not staticEndpoint then
      local playerPos = player:GetWorldPosition()
      local distToTarget = playerPos:Distance(lastStartPoint)
      if distToTarget <= max_leadDistanceThreshold then
        staticEndpoint = true
        initialEndpoint = playerPos
      end
    end
    if finalEndDestination ~= nil then
      lastStartPoint = finalEndDestination
    end
    UpdateEndDestination()
    local linePoint
    if finalEndDestination ~= nil then
      if segmentCounter > segmentStraightThreshold then
        linePoint = engine.Vector.New(math.random(), 0, math.random()) * 1.5
        linePoint = finalEndDestination + linePoint
      else
        linePoint = finalEndDestination
      end
    end
    if linePoint ~= nil then
      lineEffect:AddPoint(linePoint)
      table.insert(linePoints, linePoint)
    else
      lineCompleted = true
      playerTrackingCooldownTimer:Stop()
      objectAndFXCleanupTimer:Restart()
      return
    end
    local linePointsLength = #linePoints
    if 1 < linePointsLength then
      engine.DrawLine(linePoints[linePointsLength - 1], linePoints[linePointsLength], 65280, drawDuration)
    end
    drawLineCooldownTimer:Restart()
    if postDrawDamage then
      table.insert(concussionLocations, linePoint)
    else
      if owner ~= nil then
        local concussionParams = {
          Tweak = damagePlayerConcussion,
          WorldLocation = linePoint,
          Creature = owner,
          EnemyId = owner:GetID()
        }
        game.Combat.PlayConcussion(concussionParams)
      end
      detonationLineEffect = game.FX.Spawn(fxStage3, thisLevel, {AutoDelete = true})
    end
  end
end
function GetNextPointAlongNavPath()
  if currentPathLocation == 1 or (lastStartPoint - pathToPlayerLocations[currentPathLocation]):Length() <= randomLengthFactor[2] then
    if currentPathLocation < #pathToPlayerLocations then
      currentPathLocation = currentPathLocation + 1
    else
      pathAtEnd = true
      return initialEndpoint
    end
  end
  return pathToPlayerLocations[currentPathLocation]
end
function GetRandomFloat(num1, num2)
  local num1Mult = num1 * 1000
  local num2Mult = num2 * 1000
  local myRandomMult = math.random(num1Mult, num2Mult)
  return myRandomMult / 1000
end
function GetRandomSign()
  local random = math.random(1, 2)
  if random == 1 then
    return 1
  elseif random == 2 then
    return -1
  end
end
function StorePredictedLocationInfo(numFramesToStore)
  local distanceFromLastPoint = (lastLocation - playerLocation):Length()
  local lastDirection = (playerLocation - lastLocation):Normalized()
  local lastFrameTime = player:GetUnitTime()
  table.insert(playerInfo.playerDirectionVecValues, lastDirection)
  if numFramesToStore <= #playerInfo.playerDirectionVecValues then
    table.remove(playerInfo.playerDirectionVecValues, 1)
  end
  table.insert(playerInfo.playerTravelDistanceValues, distanceFromLastPoint)
  lastLocation = playerLocation
  if numFramesToStore <= #playerInfo.playerTravelDistanceValues then
    table.remove(playerInfo.playerTravelDistanceValues, 1)
  end
  table.insert(playerInfo.elapsedTimeValues, lastFrameTime)
  if numFramesToStore <= #playerInfo.elapsedTimeValues then
    table.remove(playerInfo.elapsedTimeValues, 1)
  end
end
function GetPredictedPlayerLocation(timeToReachPointAtCurrentVelocity)
  if timeToReachPointAtCurrentVelocity <= 0 then
    timeToReachPointAtCurrentVelocity = 1
    print("Witch Fissure - playerLeadTime cannot be less than 0")
  end
  local playerDirectionVec = newVec
  for i = 1, #playerInfo.playerDirectionVecValues do
    playerDirectionVec = playerDirectionVec + playerInfo.playerDirectionVecValues[i]
  end
  local elapsedTime = 0
  for i = 1, #playerInfo.elapsedTimeValues do
    elapsedTime = elapsedTime + playerInfo.elapsedTimeValues[i]
  end
  local playerTravelDistance = 0
  for i = 1, #playerInfo.playerTravelDistanceValues do
    playerTravelDistance = playerTravelDistance + playerInfo.playerTravelDistanceValues[i]
  end
  local playerMovementRate = playerTravelDistance / elapsedTime
  if playerMovementRate > playerMovementRateCap then
    playerMovementRate = playerMovementRateCap
  end
  local lineLength = playerMovementRate * timeToReachPointAtCurrentVelocity
  local myNormDirectionVec = player:GetWorldPosition() + playerDirectionVec:Normalized()
  local finalPos = LD.GetPointAlongLine(player:GetWorldPosition(), myNormDirectionVec, lineLength)
  return finalPos
end
function StopLineDrawing()
  canKillLine = true
  if lineEffect ~= nil and lineEffect.Spawned then
    lineEffect:Remove()
    lineEffect = nil
  end
  if preDetonationLineEffect ~= nil and preDetonationLineEffect.Spawned then
    preDetonationLineEffect:Remove()
  end
end
function TriggerPreDetonationFX()
  if lineEffect ~= nil then
    lineEffect:Remove()
    lineEffect = nil
  end
  preDetonationLineEffect = game.FX.Spawn(fxStage2)
  for i = 1, #linePoints do
    preDetonationLineEffect:AddPoint(linePoints[i])
  end
  detonationEffectTimer:Restart()
end
function TriggerDetonationFX()
  if preDetonationLineEffect ~= nil then
    preDetonationLineEffect:Remove()
    preDetonationLineEffect = nil
  end
  if thisObj ~= nil then
    detonationLineEffect = game.FX.Spawn(fxStage3, thisLevel, {AutoDelete = true})
    for i = 1, #linePoints do
      detonationLineEffect:AddPoint(linePoints[i])
    end
  end
  detonationDamageTimer:Restart()
end
function TriggerDamage()
  if thisObj ~= nil and owner ~= nil then
    local nearbyFriendlies = owner:FindFriendlies(40)
    for i = 1, #concussionLocations do
      local distanceToPlayer = (concussionLocations[i] - player:GetWorldPosition()):Length()
      if distanceToPlayer <= concussionTriggerDistance then
        local concussionParams = {
          Tweak = damagePlayerConcussion,
          WorldLocation = concussionLocations[i],
          Creature = owner,
          EnemyId = owner:GetID()
        }
        game.Combat.PlayConcussion(concussionParams)
      end
      local son = game.AI.FindSon()
      if son ~= nil then
        local distanceToSon = (concussionLocations[i] - son:GetWorldPosition()):Length()
        if distanceToSon <= 2 then
          local sonConcussionParams = {
            Tweak = aiReactionConcussion,
            WorldLocation = concussionLocations[i],
            Creature = owner,
            EnemyId = owner:GetID()
          }
          game.Combat.PlayConcussion(sonConcussionParams)
        end
      end
      if nearbyFriendlies ~= nil and 0 < #nearbyFriendlies then
        for _, creature in ipairs(nearbyFriendlies) do
          if 0 < creature:GetHitPoints() then
            local distanceToCreature = (concussionLocations[i] - creature:GetWorldPosition()):Length()
            if distanceToCreature <= 1.5 then
              local aiConcussionParams = {
                Tweak = aiReactionConcussion,
                WorldLocation = concussionLocations[i],
                Creature = owner,
                EnemyId = owner:GetID()
              }
              game.Combat.PlayConcussion(aiConcussionParams)
            end
          end
        end
      end
    end
    objectAndFXCleanupTimer:Restart()
  end
end
function ObjectAndFXCleanup()
  if thisObj ~= nil then
    thisObj:Destroy()
  end
end
local lineGO, SNDLine
function SoundInit()
  lineGO = thisObj:FindSingleGOByName("Line")
  if lineGO ~= nil then
    SNDLine = lineGO:FindSingleSoundEmitterByName("SNDLine")
  else
    print("//////////////////////// " .. tostring(lineGO) .. " is nil")
  end
end
function PlayLineOneShot()
  LD.PlaySound(SNDLine, "SND_CHR_Witch_Helwalker_RakeFissure_Line")
end
function UpdateLineGOPosition()
  if lineGO ~= nil then
    LD.TrackEmitterOnLine(linePoints, player, lineGO, 100)
  else
    print("//////////////////////// " .. tostring(lineGO) .. " is nil")
  end
end
