local LD = require("design.LevelDesignLibrary")
local thisObj, thisLevel, player, GO_Idle, GO_Reaction, GO_Reaction_Collision, GO_CW, GO_CCW, GO_CW_Collision, GO_CCW_Collision
local steps = 2
local playRate = 1
local idlePlayRate = 1
local reactionPlayRate = 1
local idleStartDelay = 0
local framesPerStep
local bIdleEnabled = false
local bReactionEnabled = false
local bRotationEnabled = false
local bCWRotationEnabled = true
local bCCWRotationEnabled = true
local bDisableDuringRotation = false
local rotateContext
local currentStep_CW = 0
local currentStep_CCW = 0
local OnRotateCallbacks, OnRotateCWCallbacks, OnRotateCCWCallbacks, OnReactionCallbacks, OnRotateCallbackList, MainObject
local isRotating = "IDLE"
local isResetting = false
local OnResetCallbacks
local resetPlayRate = 2
function OnScriptLoaded(level, obj)
  thisObj = obj
  thisLevel = level
  MainObject = thisObj.Parent.Parent
  player = game.Player.FindPlayer()
  local temp_GO_Idle = thisObj:GetLuaTableAttribute("idleObject")
  local temp_GO_Reaction = thisObj:GetLuaTableAttribute("reactionObject")
  local temp_GO_CW = thisObj:GetLuaTableAttribute("cwObject")
  local temp_GO_CCW = thisObj:GetLuaTableAttribute("ccwObject")
  local temp_GO_CW_Collision = thisObj:GetLuaTableAttribute("cwObject_Collision")
  local temp_GO_CCW_Collision = thisObj:GetLuaTableAttribute("ccwObject_Collision")
  local temp_GO_Reaction_Collision = thisObj:GetLuaTableAttribute("reactionObject_Collision")
  if temp_GO_Idle then
    GO_Idle = MainObject:FindSingleGOByName(temp_GO_Idle)
  end
  if temp_GO_Reaction then
    GO_Reaction = MainObject:FindSingleGOByName(temp_GO_Reaction)
  end
  if temp_GO_CW then
    GO_CW = MainObject:FindSingleGOByName(temp_GO_CW)
  else
    engine.Warning("cwObject missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  if temp_GO_CCW then
    GO_CCW = MainObject:FindSingleGOByName(temp_GO_CCW)
  else
    engine.Warning("ccwObject missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  if temp_GO_CW_Collision then
    GO_CW_Collision = MainObject:FindSingleGOByName(temp_GO_CW_Collision)
  else
    engine.Warning("cwObject_Collision missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  if temp_GO_CCW_Collision then
    GO_CCW_Collision = MainObject:FindSingleGOByName(temp_GO_CCW_Collision)
  else
    engine.Warning("ccwObject_Collision missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  if temp_GO_Reaction_Collision then
    GO_Reaction_Collision = MainObject:FindSingleGOByName(temp_GO_Reaction_Collision)
  else
    engine.Warning("rotateObject_Collision missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  steps = thisObj:GetLuaTableAttribute("steps")
  if steps == nil then
    engine.Warning("steps missing on object " .. thisObj:GetName() .. " in level " .. thisLevel.Name)
  end
  playRate = thisObj:GetLuaTableAttribute("playRate") or 1
  idlePlayRate = thisObj:GetLuaTableAttribute("idlePlayRate") or 1
  reactionPlayRate = thisObj:GetLuaTableAttribute("reactionPlayRate") or 1
  resetPlayRate = playRate
  idleStartDelay = thisObj:GetLuaTableAttribute("idleStartDelay") or 0
  bIdleEnabled = thisObj:GetLuaTableAttribute("bIdleEnabled")
  bRotationEnabled = thisObj:GetLuaTableAttribute("bRotationEnabled")
  bReactionEnabled = thisObj:GetLuaTableAttribute("bReactionEnabled")
  bDisableDuringRotation = thisObj:GetLuaTableAttribute("bDisableDuringRotation")
  rotateContext = thisObj:GetLuaTableAttribute("rotateContext")
  OnRotateCallbacks = thisObj:GetLuaTableAttribute("OnRotateCallbacks")
  OnReactionCallbacks = thisObj:GetLuaTableAttribute("OnReactionCallbacks")
  OnRotateCallbacks = LD.ExtractCallbacksForEvent(thisLevel, thisObj, OnRotateCallbacks)
  OnReactionCallbacks = LD.ExtractCallbacksForEvent(thisLevel, thisObj, OnReactionCallbacks)
  game.SubObject.Sleep(thisObj)
  SoundInit()
end
function OnSaveCheckpoint(level, obj)
  return {
    bIdleEnabled = bIdleEnabled,
    bReactionEnabled = bReactionEnabled,
    bRotationEnabled = bRotationEnabled,
    bCWRotationEnabled = bCWRotationEnabled,
    bCCWRotationEnabled = bCCWRotationEnabled,
    currentStep_CW = currentStep_CW,
    currentStep_CCW = currentStep_CCW
  }
end
function OnRestoreCheckpoint(level, obj, savedInfo)
  bIdleEnabled = savedInfo.bIdleEnabled
  bReactionEnabled = savedInfo.bReactionEnabled
  bRotationEnabled = savedInfo.bRotationEnabled
  bCWRotationEnabled = savedInfo.bCWRotationEnabled
  bCCWRotationEnabled = savedInfo.bCCWRotationEnabled
  currentStep_CW = savedInfo.currentStep_CW
  currentStep_CCW = savedInfo.currentStep_CCW
end
function OnStart(level, obj)
  if bIdleEnabled then
    EnableIdle()
  else
    DisableIdle()
  end
  framesPerStep = GO_CW.AnimLengthFrames / steps
  if GO_Reaction_Collision then
    GO_Reaction_Collision.LuaObjectScript.RegisterOnHitByWeapon(TriggerReaction)
  end
  if rotateContext == "Hit" then
    GO_CW_Collision.LuaObjectScript.RegisterOnHitByWeapon(RotateCW)
    GO_CCW_Collision.LuaObjectScript.RegisterOnHitByWeapon(RotateCCW)
    GO_CW_Collision.LuaObjectScript.RegisterOnHitByWeapon(TriggerReaction)
    GO_CCW_Collision.LuaObjectScript.RegisterOnHitByWeapon(TriggerReaction)
  elseif rotateContext == "Embed" then
    GO_CW_Collision.LuaObjectScript.RegisterOnWeaponEmbed(RotateCW)
    GO_CCW_Collision.LuaObjectScript.RegisterOnWeaponEmbed(RotateCCW)
    GO_CW_Collision.LuaObjectScript.RegisterOnWeaponEmbed(TriggerReaction)
    GO_CCW_Collision.LuaObjectScript.RegisterOnWeaponEmbed(TriggerReaction)
  end
  if bRotationEnabled then
    EnableRotation()
  else
    DisableIdle()
  end
end
function SoftSave()
  game.SubObject.SoftSave(thisObj)
end
function ResolveSoftSave(newStep)
  GO_CCW:JumpAnimToFrame(0)
  GO_CCW:PauseAnim()
  if framesPerStep == nil then
    framesPerStep = GO_CW.AnimLengthFrames / steps
  end
  local toFrame = newStep * framesPerStep
  GO_CW:JumpAnimToFrame(toFrame)
  GO_CW:PauseAnim()
end
function EnableIdle()
  if GO_Idle then
    if 0 < idleStartDelay then
      local timer = require("level.timer")
      timer.StartLevelTimer(idleStartDelay, GO_Idle:PlayAnimCycle(idlePlayRate))
    else
      GO_Idle:PlayAnimCycle(idlePlayRate)
    end
  end
end
function DisableIdle()
  if GO_Idle then
    GO_Idle:PauseAnim()
  end
end
function EnableReaction()
  bReactionEnabled = true
end
function DisableReaction()
  bReactionEnabled = false
end
function EnableRotation()
  bRotationEnabled = true
  GO_CW_Collision:ShowCollision()
  GO_CCW_Collision:ShowCollision()
end
function DisableRotation()
  bRotationEnabled = false
  GO_CW_Collision:HideCollision()
  GO_CCW_Collision:HideCollision()
end
function EnableCWRotation()
  bCWRotationEnabled = true
  GO_CW_Collision:ShowCollision()
end
function DisableCWRotation()
  bCWRotationEnabled = false
  GO_CW_Collision:HideCollision()
end
function EnableCCWRotation()
  bCCWRotationEnabled = true
  GO_CCW_Collision:ShowCollision()
end
function DisableCCWRotation()
  bCCWRotationEnabled = false
  GO_CCW_Collision:HideCollision()
end
function TriggerReaction()
  if bReactionEnabled then
    TriggerReactionCallbacks()
    if GO_Reaction ~= nil then
      GO_Reaction:JumpAnimToFrame(0)
      GO_Reaction:PlayAnimToEnd(reactionPlayRate)
    end
  end
end
function RotateCW(_, _, _, _, forceRotation)
  if AxeHitRegistered() then
    return
  end
  if (not bRotationEnabled or not bCWRotationEnabled) and (forceRotation == nil or forceRotation == false) then
    return
  end
  if bDisableDuringRotation and isRotating == "CW" then
    local cstep = currentStep_CW
    if cstep == 0 then
      cstep = steps
    end
    if cstep / steps - GO_CW.AnimPercent > 0.2 then
      return
    end
  end
  local fromFrame = currentStep_CW * framesPerStep
  currentStep_CW = currentStep_CW + 1
  if currentStep_CW >= steps then
    currentStep_CW = 0
  end
  local toFrame = currentStep_CW * framesPerStep
  if GO_CW.AnimFrame ~= fromFrame then
    GO_CW:JumpAnimToFrame(fromFrame)
  end
  GO_CW:PlayAnimToFrame(toFrame, playRate)
  if OnRotateCWCallbacks ~= nil then
    for i = 1, #OnRotateCWCallbacks do
      OnRotateCWCallbacks[i](GetCurrentState())
    end
  end
  TriggerRotateCallbacks(GetCurrentState())
  PlayHitSound()
  SetSoundRotationDirection("CW")
  isRotating = "CW"
  GO_CW:OnAnimDone(thisObj, "OnComplete_RotateCW")
end
function RotateCCW(_, _, _, _, forceRotation)
  if AxeHitRegistered() then
    return
  end
  if (not bRotationEnabled or not bCCWRotationEnabled) and (forceRotation == nil or forceRotation == false) then
    return
  end
  if bDisableDuringRotation and isRotating == "CCW" then
    local cstep = currentStep_CCW
    if cstep == 0 then
      cstep = steps
    end
    if cstep / steps - GO_CCW.AnimPercent > 0.2 then
      return
    end
  end
  local fromFrame = currentStep_CCW * framesPerStep
  currentStep_CCW = currentStep_CCW + 1
  if currentStep_CCW >= steps then
    currentStep_CCW = 0
  end
  local toFrame = currentStep_CCW * framesPerStep
  if GO_CCW.AnimFrame ~= fromFrame then
    GO_CCW:JumpAnimToFrame(fromFrame)
  end
  GO_CCW:PlayAnimToFrame(toFrame, playRate)
  if OnRotateCCWCallbacks ~= nil then
    for i = 1, #OnRotateCCWCallbacks do
      OnRotateCCWCallbacks[i](GetCurrentState())
    end
  end
  TriggerRotateCallbacks(GetCurrentState())
  PlayHitSound()
  SetSoundRotationDirection("CCW")
  isRotating = "CCW"
  GO_CCW:OnAnimDone(thisObj, "OnComplete_RotateCCW")
end
function OnComplete_RotateCW()
  PlayRotateStopSound("CW")
  isRotating = "IDLE"
  if isResetting == true then
    ResetRotateCW()
  end
end
function OnComplete_RotateCCW()
  PlayRotateStopSound("CCW")
  isRotating = "IDLE"
  if isResetting == true then
    ResetRotateCCW()
  end
end
function ResetRotateCW()
  if isResetting == true then
    if currentStep_CW == currentStep_CCW then
      playRate = resetPlayRate
      isResetting = false
      GO_CW:ClearAllAnimCallbacks()
      TriggerOnResetCallbacks()
    else
      playRate = playRate * 1.5
      RotateCW(nil, nil, nil, nil, true)
    end
    return true
  elseif isResetting == false and currentStep_CW ~= currentStep_CCW then
    isResetting = true
    playRate = playRate * 1.5
    RotateCW(nil, nil, nil, nil, true)
    PlayResetSound()
    return true
  else
    playRate = resetPlayRate
    return false
  end
end
function ResetRotateCCW()
  if isResetting == true then
    if currentStep_CCW == currentStep_CW then
      playRate = resetPlayRate
      isResetting = false
      GO_CCW:ClearAllAnimCallbacks()
      TriggerOnResetCallbacks()
    else
      playRate = playRate * 1.5
      RotateCCW(nil, nil, nil, nil, true)
    end
    return true
  elseif isResetting == false and currentStep_CCW ~= currentStep_CW then
    isResetting = true
    playRate = playRate * 2.2
    RotateCCW(nil, nil, nil, nil, true)
    PlayResetSound()
    return true
  else
    playRate = resetPlayRate
    return false
  end
end
function RegisterOnResetCallback(fn)
  if OnResetCallbacks == nil then
    OnResetCallbacks = {}
  end
  if fn ~= nil then
    table.insert(OnResetCallbacks, fn)
  end
end
function TriggerOnResetCallbacks()
  if OnResetCallbacks ~= nil then
    for i = 1, #OnResetCallbacks do
      OnResetCallbacks[i]()
    end
  end
end
function TriggerReactionCallbacks()
  if OnReactionCallbacks ~= nil then
    LD.ExecuteCallbacksForEvent(thisLevel, thisObj, OnReactionCallbacks, "OnReactionCallbacks: " .. MainObject:GetName() .. " in level " .. thisLevel.Name)
  end
end
function TriggerRotateCallbacks(newState)
  if OnRotateCallbackList ~= nil then
    for i = 1, #OnRotateCallbackList do
      OnRotateCallbackList[i](newState)
    end
  end
  if OnRotateCallbacks ~= nil then
    LD.ExecuteCallbacksForEvent(thisLevel, thisObj, OnRotateCallbacks, "OnRotateCallbacks(" .. tostring(newState) .. "): " .. MainObject:GetName() .. " in level " .. thisLevel.Name, newState)
  end
end
function GetCurrentState()
  local step = currentStep_CW - currentStep_CCW + 1
  if step <= 0 then
    step = steps + step
  end
  return step
end
function AxeHitRegistered()
  if player == nil or player.Axe == nil then
    return true
  end
  if player.Axe.ThrowOutStatus == tweaks.tThrowOutStatus.eThrownWeaponStatus.kTOSInFlightReturn or player.Axe.ThrowOutStatus == tweaks.tThrowOutStatus.eThrownWeaponStatus.kTOSSuspended or player.Axe.ThrowOutStatus == tweaks.tThrowOutStatus.eThrownWeaponStatus.kTOSFalling then
    return true
  end
  return false
end
function GetCWRotationEnabled()
  return bCWRotationEnabled
end
function GetCCWRotationEnabled()
  return bCCWRotationEnabled
end
function RegisterOnRotateCallback(fn)
  if OnRotateCallbackList == nil then
    OnRotateCallbackList = {}
  end
  if fn ~= nil then
    table.insert(OnRotateCallbackList, fn)
  end
end
function RegisterOnRotateCWCallback(fn)
  if OnRotateCWCallbacks == nil then
    OnRotateCWCallbacks = {}
  end
  if fn ~= nil then
    table.insert(OnRotateCWCallbacks, fn)
  end
end
function RegisterOnRotateCCWCallback(fn)
  if OnRotateCCWCallbacks == nil then
    OnRotateCCWCallbacks = {}
  end
  if fn ~= nil then
    table.insert(OnRotateCCWCallbacks, fn)
  end
end
local soundEmitter
local soundAllowHitSoundsOnReset = false
local soundCurrentRotationDirection = "IDLE"
local soundEvents = {
  OnHit = "SND_MECH_Rotary_Turnstile_Puzzle_Hit",
  OnReset = "",
  OnRotateStop = ""
}
function SoundInit()
  soundEmitter = thisObj.Parent.Parent:FindSingleSoundEmitterByName("SNDSpinner")
end
function SoundSetup(sounds)
  if sounds ~= nil then
    if sounds.SoundEmitter ~= nil then
      soundEmitter = thisObj.Parent.Parent:FindSingleSoundEmitterByName(sounds.SoundEmitter)
    end
    if sounds.AllowHitSoundsOnReset ~= nil then
      soundAllowHitSoundsOnReset = sounds.AllowHitSoundsOnReset
    end
    for key in pairs(soundEvents) do
      for newKey, newValue in pairs(sounds) do
        if newKey == key and newValue ~= nil and newValue ~= "" then
          soundEvents[key] = newValue
        end
      end
      LD.SoundDebug(tostring(key) .. ": " .. tostring(soundEvents[key]))
    end
  end
end
function PlayHitSound()
  if AllowHitSound() then
    LD.PlaySound(soundEmitter, soundEvents.OnHit)
  end
end
function AllowHitSound()
  if isResetting then
    if soundAllowHitSoundsOnReset then
      return true
    else
      return false
    end
  else
    return true
  end
end
function SetSoundRotationDirection(newDirection)
  soundCurrentRotationDirection = string.upper(tostring(newDirection))
end
function PlayRotateStopSound(targetDirection)
  if soundCurrentRotationDirection == string.upper(tostring(targetDirection)) then
    LD.PlaySound(soundEmitter, soundEvents.OnRotateStop)
    SetSoundRotationDirection("IDLE")
  end
end
function PlayResetSound()
  LD.PlaySound(soundEmitter, soundEvents.OnReset)
end
function GetDebugText()
  local debugText = "FlipperRefNode: " .. thisObj.Parent:GetName()
  debugText = debugText .. [[

>CW state ]] .. tostring(currentStep_CW)
  debugText = debugText .. [[

>CCW state  ]] .. tostring(currentStep_CCW)
  debugText = debugText .. [[

>Rotation Enable ]] .. tostring(bRotationEnabled)
  return debugText
end
function ShowDebugText()
  local debugText = GetDebugText()
  engine.DrawTextInWorld(thisObj:GetWorldPosition(), debugText)
end
