local profile = require("core.profile")
local CDBGL = require("camera.CameraDebugLibrary")
local dbgPenetrationValue = 0
local dbgPenetratedQuadrant = 0
local dbgQuadObject = 3.73592858E9
local largeCreatures = {
  "troll00",
  "troll10",
  "troll20",
  "jotunn00",
  "jotunn10",
  "jotunn20"
}
local kPointFive = 0.05
local kFifty = 0.5
local kSeventyFive = 0.75
local kMagicFudge = 0.05
local kColorPurple = 5577355
local kColorRed = 16711680
local kColorGreen = 25600
local kColorBlue = 255
local kColorWhite = 16777215
local kTopLeftX = -1
local kTopRightX = 1
local kBottomLeftX = -1
local kBottomRightX = 1
local kTopLeftY = 1
local kTopRightY = 1
local kBottomLeftY = -1
local kBottomRightY = -1
local kCenterX = 0
local kCenterY = 0
local kTopHalf = 1
local kBottomHalf = 2
local kLeftHalf = 3
local kRightHalf = 4
local dbgDrawLockon = false
local FindPlayerSafetyCheck = function(playerCreature)
  if playerCreature == nil then
    playerCreature = game.Player.FindPlayer()
    return playerCreature
  else
    return playerCreature
  end
end
local round = function(num, idp)
  if num == nil then
    return
  end
  local mult = 10 ^ (idp or 0)
  return math.floor(num * mult + 0.5) / mult
end
local NumCreaturesBelowPenetration = function(creaturesNearby, frustumPenetration)
  local enemyPosition
  local offscreenThreats = 0
  local enemyFrustumPenetrate
  for _, thisEnemy in ipairs(creaturesNearby) do
    if thisEnemy:IsDead() == false then
      enemyPosition = thisEnemy:GetWorldPosition()
      enemyFrustumPenetrate = game.Camera.GetViewPenetration(enemyPosition, kCenterX, kCenterY)
      if frustumPenetration > enemyFrustumPenetrate then
        offscreenThreats = offscreenThreats + 1
      end
    end
  end
  return offscreenThreats
end
local DEG = 180 / math.pi
local PI2 = math.pi * 2
local PI4 = math.pi * 4
local fmod = math.fmod
local atan2 = math.atan2
local NormalizeAngle = function(a)
  local b = fmod(PI4 + a, PI2)
  return b
end
local FrontAngleXZ = function(targetsworldposition)
  local fwd = game.Camera.GetOrbitForward()
  local to = (targetsworldposition - game.Camera.GetOrbitPosition()):Normalized()
  local afwd = atan2(fwd.z, fwd.x)
  local ato = atan2(to.z, to.x)
  local angle = NormalizeAngle(ato - afwd) * DEG
  if 180 < angle then
    angle = angle - 360
  end
  return angle
end
local FrontAngleYZ = function(targetsworldposition)
  local origin = game.Camera.GetOrbitPosition()
  local toVector = targetsworldposition - origin
  local height = targetsworldposition.y - origin.y
  local length = toVector:Length()
  local soh = height / length
  local angleRadians = math.asin(soh)
  local angleDegree = math.deg(angleRadians)
  if dbgDrawLockon then
    local thirdCoordinate = engine.Vector.New(targetsworldposition.x, origin.y, targetsworldposition.z)
    if CDBGL.IsCircleReleased() then
      CDBGL.SetLockonCameraPos(origin)
      CDBGL.SetLockonTargetPos(targetsworldposition)
      CDBGL.SetOrbitForward(game.Camera.GetOrbitForward())
      CDBGL.SetLockonBottomPos(thirdCoordinate)
    end
    local playerCreature = game.Player.FindPlayer()
    if playerCreature.Pad.SquareDown then
      local camPos = CDBGL.GetLockonCameraPos()
      local targetPos = CDBGL.GetLockonTargetPos()
      if camPos ~= nil and targetPos ~= nil then
        local bottomPos = CDBGL.GetLockonBottomPos()
        engine.DrawLine(camPos, bottomPos, 65280)
        engine.DrawLine(camPos, targetPos, 65280)
        engine.DrawLine(targetPos, bottomPos, 983280)
        CDBGL.DrawBetterSphere(camPos, 0.2, 65280)
        local orbitForward = CDBGL.GetOrbitForward()
        if orbitForward ~= nil then
          engine.DrawLine(camPos, camPos + orbitForward * 10, 16711680)
        end
      end
    end
    local forwardVector = thirdCoordinate - origin
    forwardVector = forwardVector:Normalized()
    local orbitForward = game.Camera.GetOrbitForward()
    local cameraDotPosition = forwardVector:Dot(orbitForward)
    local cosValue = math.acos(cameraDotPosition)
    local fakeDegree = math.deg(cosValue)
    return angleDegree, angleRadians, fakeDegree
  end
  return angleDegree, angleRadians
end
local NumLargeCreaturesNearby = function(creaturesNearby, distance)
  local enemyPosition, enemyName
  local largeThreats = 0
  local enemyDistance
  local playerCreature = game.Player.FindPlayer()
  local playerPosition = playerCreature:GetWorldPosition()
  for _, thisEnemy in ipairs(creaturesNearby) do
    enemyName = thisEnemy:GetName()
    if thisEnemy:IsDead() == false and enemyName == "jotunn10" then
      enemyPosition = thisEnemy:GetWorldPosition()
      enemyDistance = enemyPosition:Distance(playerPosition)
      if distance > enemyDistance then
        largeThreats = largeThreats + 1
      end
    end
  end
  return largeThreats
end
local Clamp = function(low, n, high)
  return math.min(math.max(n, low), high)
end
local PopulateCameraTable = function(camera)
  local cameraMembers = {
    "name",
    "fov",
    "FStop",
    "Distance",
    "age",
    "Priority"
  }
  local cameraTable = {}
  for _, member in ipairs(cameraMembers) do
    cameraTable[member] = camera[member]
  end
  return cameraTable
end
local ElevationRange = function(inputMin, inputMax, currentElevation)
  local normalized
  normalized = (currentElevation - inputMin) / (inputMax - inputMin)
  normalized = Clamp(0, normalized, 1)
  return normalized
end
local CameraUpdateAttribute = function(camera, driver, min, max, attribute, target)
  if max < min then
    if min < driver or driver < max then
      return
    end
  elseif min < max then
    if driver < min or max < driver then
      return
    end
  elseif max == min then
    return
  end
  local scale = ElevationRange(min, max, driver)
  local property
  local scaleExponent = 2
  if attribute == "fov" then
    attribute = "AngleOfView"
  elseif attribute == "X" or attribute == "Y" or attribute == "Z" then
    property = attribute
    attribute = "Position"
  elseif attribute == "PlayerFrame.Top" then
    property = "Top"
    attribute = "PlayerFrame"
  elseif attribute == "PlayerFrame.Bottom" then
    property = "Bottom"
    attribute = "PlayerFrame"
  elseif attribute == "PlayerFrame.Left" then
    property = "Left"
    attribute = "PlayerFrame"
  elseif attribute == "PlayerFrame.Right" then
    property = "Right"
    attribute = "PlayerFrame"
  end
  if attribute == "PlayerFrame" or attribute == "Position" then
    camera[attribute][property] = (target - camera.initial[attribute][property]) * scale ^ scaleExponent + camera.initial[attribute][property]
  elseif attribute == "tilt" then
    camera.TiltUp = (target - camera.initial.TiltUp) * scale ^ scaleExponent + camera.initial.TiltUp
    camera.TiltDown = (target - camera.initial.TiltDown) * scale ^ scaleExponent + camera.initial.TiltDown
  elseif attribute == "pan" then
    camera.PanLeft = (target - camera.initial.PanLeft) * scale + camera.initial.PanLeft
    camera.PanRight = (target - camera.initial.PanRight) * scale + camera.initial.PanRight
  else
    camera[attribute] = (target - camera.initial[attribute]) * scale ^ scaleExponent + camera.initial[attribute]
  end
end
local BlendAttributeWeight = function(targetA, scaleA, targetB, scaleB, original)
  local blendedAttribute
  blendedAttribute = (targetA - original) * scaleA ^ 2 + (targetB - original) * scaleB ^ 2 + original
  return blendedAttribute
end
local UpdateCameraProperties = function(camera, scale, distance, fov, tilt, playerFrameRight, playerFrameLeft, roll, playerFrameTop, playerFrameBottom, x, y)
  if distance ~= nil then
    camera.Distance = (distance - camera.initial.Distance) * scale ^ 2 + camera.initial.Distance
  end
  if fov ~= nil then
    camera.fov = (fov - camera.initial.AngleOfView) * scale ^ 2 + camera.initial.AngleOfView
  end
  if playerFrameRight ~= nil then
    camera.PlayerFrame.Right = (playerFrameRight - camera.initial.PlayerFrame.Right) * scale ^ 3 + camera.initial.PlayerFrame.Right
  end
  if playerFrameLeft ~= nil then
    camera.PlayerFrame.Left = (playerFrameLeft - camera.initial.PlayerFrame.Left) * scale ^ 3 + camera.initial.PlayerFrame.Left
  end
  if playerFrameTop ~= nil then
    camera.PlayerFrame.Top = (playerFrameTop - camera.initial.PlayerFrame.Top) * scale ^ 2 + camera.initial.PlayerFrame.Top
  end
  if playerFrameBottom ~= nil then
    camera.PlayerFrame.Bottom = (playerFrameBottom - camera.initial.PlayerFrame.Bottom) * scale ^ 2 + camera.initial.PlayerFrame.Bottom
  end
  if tilt ~= nil then
    camera.TiltUp = (tilt - camera.initial.TiltUp) * scale ^ 2 + camera.initial.TiltUp
    camera.TiltDown = (tilt - camera.initial.TiltDown) * scale ^ 2 + camera.initial.TiltDown
  end
  if roll ~= nil then
    camera.Roll = (roll - camera.initial.Roll) * scale ^ 2 + camera.initial.Roll
  end
  if x ~= nil then
    camera.Position.X = (x - camera.initial.Position.X) * scale ^ 2 + camera.initial.Position.X
  end
  if y ~= nil then
    camera.Position.Y = (y - camera.initial.Position.Y) * scale ^ 2 + camera.initial.Position.Y
  end
end
local ElevationAdjust = function(camera, inputMin, inputMax, currentElevation, distance, fov, x, y, z, tilt, pan, roll, playerFrameRight, playerFrameLeft, playerFrameTop, playerFrameBottom)
  local scale = ElevationRange(inputMin, inputMax, currentElevation)
  if inputMax < inputMin then
    if currentElevation <= inputMin and inputMax <= currentElevation then
      UpdateCameraProperties(camera, scale, distance, fov, tilt, playerFrameRight, playerFrameLeft, roll, playerFrameTop, playerFrameBottom, x, y)
    end
  elseif inputMin <= currentElevation and currentElevation <= inputMax then
    UpdateCameraProperties(camera, scale, distance, fov, tilt, playerFrameRight, playerFrameLeft, roll, playerFrameTop, playerFrameBottom, x, y)
  end
end
local IsInFrontOfPlayer = function(objposition, bPrint)
  local objectPosition = objposition
  local primeCam = game.Camera.GetPrimary()
  local cameraPosition = game.Camera.GetOrbitPosition()
  local cameraVectorToPosition = objectPosition - cameraPosition
  local forwardVector = game.Camera.GetOrbitForward()
  local cameraDotPosition = forwardVector:Dot(cameraVectorToPosition:Normalized())
  local cosValue = math.acos(cameraDotPosition)
  local degValue = math.deg(cosValue)
  local bInFront = false
  local fovHalf = primeCam.fov / 2
  degValue = degValue + kMagicFudge
  if fovHalf > degValue then
    bInFront = true
  end
  return bInFront
end
local GetPenetrationColor = function(penValue)
  local color = kColorBlue
  if penValue > kPointFive then
    color = kColorGreen
  end
  if penValue > kFifty then
    color = kColorRed
  end
  if penValue > kSeventyFive then
    color = kColorPurple
  end
  return color
end
local GetQuadrantCenters = function(quadrant)
  if quadrant == 1 then
    return kTopRightX, kTopRightY
  elseif quadrant == 2 then
    return kTopLeftX, kTopLeftY
  elseif quadrant == 3 then
    return kBottomLeftX, kBottomLeftY
  elseif quadrant == 4 then
    return kBottomRightX, kBottomRightY
  else
    return kCenterX, kCenterY
  end
end
local GetQuadrantHalves = function(side)
  if side == kTopHalf then
    return 1, 2, "TOP HALF"
  elseif side == kBottomHalf then
    return 3, 4, "BOTTOM HALF"
  elseif side == kLeftHalf then
    return 2, 3, "LEFT SIDE"
  elseif side == kRightHalf then
    return 1, 4, "RIGHT SIDE"
  else
    assert(false, "::GetQuadrantHalves() ... not TOP/BOTTOM/LEFT/RIGHT")
  end
end
local DrawCountersAndBox = function(x, y, fPenetration, bPrint)
  local screenCenterX = x
  local screenCenterY = y
  local penetrationColor = GetPenetrationColor(fPenetration)
  game.Camera.DrawPenetrationContour(screenCenterX, screenCenterY, kFifty, kColorWhite)
  local penetrationBox = 1 - fPenetration
  game.Camera.DrawPenetrationContour(screenCenterX, screenCenterY, penetrationBox, penetrationColor)
end
local DrawPenetratedQuadrant = function(bPrint)
  if dbgPenetrationValue > kFifty then
    local x, y = GetQuadrantCenters(dbgPenetratedQuadrant)
    DrawCountersAndBox(x, y, dbgPenetrationValue, bPrint)
  end
end
local CheckQuadrantPenetrationByWorldPosition = function(worldPos, x, y, bPrint)
  local screenCenterX = x
  local screenCenterY = y
  local fPenetrationFactor = game.Camera.GetViewPenetration(worldPos, screenCenterX, screenCenterY)
  return fPenetrationFactor
end
local FindPenetratedQuadrant = function(object, bPrint)
  local currentQuadrant, screenCenterX, screenCenterY, penetrationValue
  assert(object, "FindPenetratedQuadrant(object)")
  dbgQuadObject = object
  local worldPos = dbgQuadObject:GetWorldPosition()
  for i = 1, 4 do
    currentQuadrant = i
    screenCenterX, screenCenterY = GetQuadrantCenters(currentQuadrant)
    penetrationValue = CheckQuadrantPenetrationByWorldPosition(worldPos, screenCenterX, screenCenterY, bPrint)
    if penetrationValue > kFifty then
      dbgPenetratedQuadrant = currentQuadrant
      dbgPenetrationValue = penetrationValue
      return currentQuadrant, penetrationValue
    end
  end
  currentQuadrant = 0
  penetrationValue = 0
  dbgPenetratedQuadrant = currentQuadrant
  dbgPenetrationValue = penetrationValue
  return currentQuadrant, penetrationValue
end
local TrackQuadrantOfObject = function(object, bPrint)
  local quadrant, penetrationValue = 0, 0
  dbgQuadObject = object
  local position = dbgQuadObject:GetWorldPosition()
  local bInFront = IsInFrontOfPlayer(position, bPrint)
  if bInFront then
    quadrant, penetrationValue = FindPenetratedQuadrant(dbgQuadObject, bPrint)
    return quadrant, penetrationValue
  end
  dbgPenetrationValue = penetrationValue
  return quadrant, penetrationValue
end
local CheckHalfsides = function(object, side, bPrint)
  assert(object, "CheckHalfsides(object)")
  dbgQuadObject = object
  local worldPos = dbgQuadObject:GetWorldPosition()
  local quadsToCheck = {}
  local currentQuadrant, screenCenterX, screenCenterY, penetrationValue
  local bIsOnHalf = false
  local strNameOfHalfside
  quadsToCheck[1], quadsToCheck[2], strNameOfHalfside = GetQuadrantHalves(side)
  for _, q in ipairs(quadsToCheck) do
    currentQuadrant = q
    screenCenterX, screenCenterY = GetQuadrantCenters(currentQuadrant)
    penetrationValue = CheckQuadrantPenetrationByWorldPosition(worldPos, screenCenterX, screenCenterY, bPrint)
    if penetrationValue > kFifty then
      bIsOnHalf = true
      dbgPenetratedQuadrant = currentQuadrant
      dbgPenetrationValue = penetrationValue
      return bIsOnHalf, currentQuadrant, strNameOfHalfside
    end
  end
  currentQuadrant = 0
  dbgPenetratedQuadrant = 0
  dbgPenetrationValue = 0
  return bIsOnHalf, currentQuadrant, strNameOfHalfside
end
local GetRGBFromHex = function(hexValue, bOverride)
  local red = bit32.band(hexValue, 16711680)
  red = bit32.rshift(red, 16)
  local green = bit32.band(hexValue, 65280)
  green = bit32.rshift(green, 8)
  local blue = bit32.band(hexValue, 255)
  return red, green, blue
end
local DisplayQuadrantDebugTable = function()
  local debugPenetrationTable = {}
  local debugObjectName = dbgQuadObject:GetName()
  local debugObjectPos = dbgQuadObject:GetWorldPosition()
  local penetrationColor = GetPenetrationColor(dbgPenetrationValue)
  debugPenetrationTable[1] = {
    "Object Name: ",
    debugObjectName
  }
  debugPenetrationTable[2] = {
    "Penetration Value: ",
    dbgPenetrationValue
  }
  debugPenetrationTable[3] = {
    "Object Position: ",
    debugObjectPos
  }
  local red, green, blue = GetRGBFromHex(penetrationColor, false)
  local tableColor = engine.Vector.New(red, green, blue)
  debugPenetrationTable.Title = "CameraLibrary: Penetration Debug Menu"
  debugPenetrationTable.TitleColor = tableColor
  debugPenetrationTable.TitleAlpha = 255
  debugPenetrationTable.X = 150
  debugPenetrationTable.Y = 59
  engine.DrawDebugTable(debugPenetrationTable)
end
local IsLargeCreature = function(creature)
  for _ in ipairs(largeCreatures) do
    if creature:GetName() == largeCreatures[_] then
      return true
    end
  end
end
local CamStickSubmission = function(stick, axis, cameraA, lowerA, upperA, cameraB, lowerB, upperB, stickTimer)
  local stickIntent
  local playerCreature = game.Player.FindPlayer()
  local pad = playerCreature.Pad
  if not pad then
    stickIntent = engine.Vector.New()
  elseif stick == "right" then
    stickIntent = pad.CorrectedRightStick
  else
    stickIntent = pad.CorrectedLeftStick
  end
  if lowerA <= stickIntent[axis] and upperA >= stickIntent[axis] or cameraB ~= "nil" and lowerB <= stickIntent[axis] and upperB >= stickIntent[axis] then
    stickTimer:Start()
    if stickTimer.fired == true then
      if lowerA <= stickIntent[axis] and upperA >= stickIntent[axis] then
        game.Camera.SubmitCameraByName(cameraA)
      elseif cameraB ~= "nil" and lowerB <= stickIntent[axis] and upperB >= stickIntent[axis] then
        game.Camera.SubmitCameraByName(cameraB)
      end
    end
  elseif stickTimer.running == true or stickTimer.fired == true then
    stickTimer:Restart()
    stickTimer:Stop()
  end
end
return profile.WrapLibrary({
  kTopHalf = kTopHalf,
  kBottomHalf = kBottomHalf,
  kLeftHalf = kLeftHalf,
  kRightHalf = kRightHalf,
  FindPlayerSafetyCheck = FindPlayerSafetyCheck,
  round = round,
  NumCreaturesBelowPenetration = NumCreaturesBelowPenetration,
  FrontAngleXZ = FrontAngleXZ,
  FrontAngleYZ = FrontAngleYZ,
  PopulateCameraTable = PopulateCameraTable,
  NumLargeCreaturesNearby = NumLargeCreaturesNearby,
  Clamp = Clamp,
  UpdateCameraProperties = UpdateCameraProperties,
  ElevationRange = ElevationRange,
  ElevationAdjust = ElevationAdjust,
  CameraUpdateAttribute = CameraUpdateAttribute,
  BlendAttributeWeight = BlendAttributeWeight,
  GetPenetrationColor = GetPenetrationColor,
  DrawCountersAndBox = DrawCountersAndBox,
  DrawPenetratedQuadrant = DrawPenetratedQuadrant,
  TrackQuadrantOfObject = TrackQuadrantOfObject,
  CheckHalfsides = CheckHalfsides,
  GetRGBFromHex = GetRGBFromHex,
  DisplayQuadrantDebugTable = DisplayQuadrantDebugTable,
  IsLargeCreature = IsLargeCreature,
  CamStickSubmission = CamStickSubmission
})
