local prefix = "otrx"

local otrx_range = CV_RegisterVar({
  name = prefix .. "_range",
  defaultvalue = "7",
  possiblevalue = {MIN = 0, MAX = 16},
  flags = CV_NETVAR|CV_SHOWMODIF
})

local otrx_spb_range = CV_RegisterVar({
  name = prefix .. "_spb_range",
  defaultvalue = "2",
  possiblevalue = {MIN = 0, MAX = 16},
  flags = CV_NETVAR|CV_SHOWMODIF
})

local otrx_beep = CV_RegisterVar({
  name = prefix .. "_beep",
  defaultvalue = "0.8",
  possiblevalue = {MIN = 0, MAX = FRACUNIT},
  flags = CV_FLOAT|CV_SHOWMODIF
})

local RF_BATTLE = 1
local RF_RACE = 1 << 1
local RF_ENCORE = 1 << 2
local RF_NONENCORE = 1 << 3
local RF_HELL = 1 << 4
local RF_NONHELL = 1 << 5

local modes_table = {
    ["Always"] = RF_BATTLE | RF_RACE | RF_ENCORE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    ["Never"] = 0,
    
    ["Battle"] = RF_BATTLE | RF_ENCORE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    ["Race"] = RF_RACE | RF_ENCORE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    
    ["Encore"] = RF_BATTLE | RF_RACE | RF_ENCORE | RF_HELL | RF_NONHELL,
    ["Non-encore"] = RF_BATTLE | RF_RACE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    
    ["Hell"] = RF_BATTLE | RF_RACE | RF_ENCORE | RF_NONENCORE | RF_HELL,
    ["Non-hell"] = RF_BATTLE | RF_RACE | RF_ENCORE | RF_NONENCORE | RF_NONHELL,
    
    ["Battle-Encore"] = RF_BATTLE | RF_ENCORE | RF_HELL | RF_NONHELL,
    ["Battle-Non-encore"] = RF_BATTLE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    ["Battle-Hell"] = RF_BATTLE | RF_ENCORE | RF_NONENCORE | RF_HELL,
    ["Battle-Non-hell"] = RF_BATTLE | RF_ENCORE | RF_NONENCORE | RF_NONHELL,
    ["Battle-Encore-Hell"] = RF_BATTLE | RF_ENCORE | RF_HELL,
    ["Battle-Encore-Non-hell"] = RF_BATTLE | RF_ENCORE | RF_NONHELL,
    ["Battle-Non-encore-Hell"] = RF_BATTLE | RF_NONENCORE | RF_HELL,
    ["Battle-Non-encore-Non-hell"] = RF_BATTLE | RF_NONENCORE | RF_NONHELL,
    
    ["Battle-Encore"] = RF_RACE | RF_ENCORE | RF_HELL | RF_NONHELL,
    ["Battle-Non-encore"] = RF_RACE | RF_NONENCORE | RF_HELL | RF_NONHELL,
    ["Battle-Hell"] = RF_RACE | RF_ENCORE | RF_NONENCORE | RF_HELL,
    ["Battle-Non-hell"] = RF_RACE | RF_ENCORE | RF_NONENCORE | RF_NONHELL,
    ["Battle-Encore-Hell"] = RF_RACE | RF_ENCORE | RF_HELL,
    ["Battle-Encore-Non-hell"] = RF_RACE | RF_ENCORE | RF_NONHELL,
    ["Battle-Non-encore-Hell"] = RF_RACE | RF_NONENCORE | RF_HELL,
    ["Battle-Non-encore-Non-hell"] = RF_RACE | RF_NONENCORE | RF_NONHELL,
  }

local otrx_random = CV_RegisterVar({
  name = prefix .. "_random",
  defaultvalue = "Race",
  possiblevalue = modes_table,
  flags = CV_NETVAR
})

local otrx_falling = CV_RegisterVar({
  name = prefix .. "_falling",
  defaultvalue = "Encore",
  possiblevalue = modes_table,
  flags = CV_NETVAR
})

rawset(_G, "OpenTricks_Table", {})
rawset(_G, "OpenTricks_Table_Pending", {})
rawset(_G, "OpenTricks_Hooks", {})
rawset(_G, "OpenTricks_TableNeedsUpdate", false)

local TICRATE = TICRATE

local function OpenTricks_Debug(input)
  print("[OpenTricks]: " .. input)
end

local function OpenTricks_CheckMode(cvar)
  local v = cvar.value
  
  local battle_allowed = (v & RF_BATTLE)
  local race_allowed = (v & RF_RACE)
  
  local encore_allowed = (v & RF_ENCORE)
  local nonencore_allowed = (v & RF_NONENCORE)
  
  local hell_allowed = (v & RF_HELL)
  local nonhell_allowed = (v & RF_NONHELL)
  
  local mode_validity = race_allowed
  if G_BattleGametype()
    mode_validity = battle_allowed
  end
  
  local encore_validity = nonencore_allowed
  if encoremode
    encore_validity = encore_allowed
  end
  
  local hell_validity = nonhell_allowed
  if mapheaderinfo[gamemap].menuflags & LF2_HIDEINMENU
    hell_validity = hell_allowed
  end
  
  return (mode_validity and encore_validity and hell_validity)
end

local function OpenTricks_GetRange(player, check_spb)
  if (player.OpenTricks.meter.blocked)
    return 0
  elseif (check_spb and spbplace == player.kartstuff[k_position])
    return otrx_spb_range.value
  end
  return otrx_range.value
end

rawset(_G, "OpenTricks_GetRange", OpenTricks_GetRange)

local function OpenTricks_Success(player)
  S_StartSoundAtVolume(player.mo, sfx_otrx1 + min(player.OpenTricks.combo, 6), 200)
  player.OpenTricks.combo = $ + 1
  OpenTricks_MakeStars(player, player.mo.color, min(player.OpenTricks.combo, 6) + 1) -- 2 stars for first trick, +1 afterwards
  if (OpenTricks_Hooks[player.OpenTricks.trick_number].successHook) -- execute success hook if it exists
    OpenTricks_Hooks[player.OpenTricks.trick_number].successHook(player)
  end
  player.OpenTricks.tricking, player.trickactive = TICRATE, true
end

local function OpenTricks_CanSelect(player)
  return (leveltime < 236) -- and not (player.cmd.buttons & BT_DRIFT) -- hack for rolling starts
end

rawset(_G, "OpenTricks_CanSelect", OpenTricks_CanSelect)

local function OpenTricks_CanTrick(player)
  local mobj = player.mo
  if (OpenTricks_GetRange(player, true) == 0) -- not disabled
    return false
  elseif (player.OpenTricks.leniency > TICRATE) -- has been rising in the air recently
    return false
  elseif (player.kartstuff[k_spinouttimer]) -- not spinning out
    return false
  elseif (player.exiting)
    return false
  elseif (mobj and mobj.valid and P_IsObjectOnGround(mobj)) -- not on the ground
    return false
  end
  return (mobj and mobj.valid and mobj.health > 0)
end

local function OpenTricks_TryingTrick(player)
  return (OpenTricks_CanTrick(player) and (player.OpenTricks.leniency < TICRATE) and (player.cmd.buttons & player.OpenTricks.button))
end

rawset(_G, "OpenTricks_TryingTrick", OpenTricks_TryingTrick)

local function OpenTricks_CanFinish(player)
  local mobj = player.mo
  if (OpenTricks_GetRange(player, true) == 0) -- not disabled
    return false
  elseif (player.kartstuff[k_spinouttimer]) -- not spinning out
    return false
  elseif (player.exiting)
    return false
  end
  return (mobj and mobj.valid and mobj.health > 0)
end

local function OpenTricks_CanSweetspot(player)
  return (abs(player.OpenTricks.meter.sweetspot - player.OpenTricks.meter.timer) <= OpenTricks_GetRange(player, true))
end

local function OpenTricks_GetValidStyles(input)
  local filtered = {}
  for key, value in pairs(input)
    if not value.banned
      filtered[key] = value
    end
  end
  return filtered
end

local function OpenTricks_Server()
  if leveltime < 236 and OpenTricks_TableNeedsUpdate
    OpenTricks_Table = OpenTricks_Table_Pending
    OpenTricks_TableNeedsUpdate = false
  end
end

local function OpenTricks_AnimateTrick(player)
  local mobj = player.mo
  if player.trickactive and (player.OpenTricks.tricking == 0 or P_IsObjectOnGround(mobj))
    player.trickactive = false
    player.OpenTricks.tricking = 0
	player.frameangle = 0
  elseif (player.OpenTricks.tricking > 0)
    if SIMPLE_ANIMAL_DEFINITIONS and SIMPLE_ANIMAL_DEFINITIONS[mobj.skin] -- Simple AnimaL takes priority.
      -- Don't touch that dial.
    elseif player.trickframes -- for legacy acrobatics characters
      mobj.frame = T 
    elseif player.cmd.driftturn < 0
      mobj.frame = P
    else
      mobj.frame = N
    end
    player.frameangle = (ANG10 * player.OpenTricks.tricking) * (player.OpenTricks.combo + 1)
    player.OpenTricks.tricking = $ - 1
  end
  
  if player.mo.ksc_overlay
    player.mo.ksc_overlay.angle = player.frameangle -- secondcolor hack
  end
  if player.rainbowcolor
    player.rainbowcolor.angle = player.frameangle -- dynaskins hack
  end
end

local function OpenTricks_MenuClamp(player)
  local max_tricks = #OpenTricks_Table
  if player.OpenTricks.trick_number < 1
	player.OpenTricks.trick_number = max_tricks
  elseif player.OpenTricks.trick_number > max_tricks
	player.OpenTricks.trick_number = 1
  end
end

local function OpenTricks_MenuInc(player, inc)
  local n = player.OpenTricks.trick_number
  player.OpenTricks.trick_number = $ + inc
  OpenTricks_MenuClamp(player)
  while OpenTricks_Table[player.OpenTricks.trick_number] and OpenTricks_Table[player.OpenTricks.trick_number].banned
	player.OpenTricks.trick_number = $ + inc
	OpenTricks_MenuClamp(player)
  end
end

local function OpenTricks_Thinker(player)
  if not OpenTricks_Table or not player or not player.valid
    return
  end
  
  if not player.OpenTricks
    player.OpenTricks = {["trick_number"] = 1, ["key"] = "dropdash", ["selected"] = false, ["selectBuffer"] = 0,
                         ["leniency"] = TICRATE, ["tricking"] = 0, ["heldLast"] = false,
                         ["button"] = BT_BRAKE,
                         ["combo"] = 0, ["meter"] = {["reversed"] = false, ["timer"] = 0, ["sweetspot"] = 16}, ["blocked"] = false}
    player.trickactive = 0
  end
  
  if (otrx_range.value == 0)
    return
  end
  
  if OpenTricks_CanSelect(player) -- manage pre-race selection
    player.OpenTricks.combo = 0
    player.OpenTricks.selected = false
    local max_tricks = #OpenTricks_Table
    if (player.cmd.buttons & (BT_DRIFT | BT_BRAKE | BT_ATTACK))
      player.OpenTricks.selectBuffer = 0 -- no.
    elseif player.cmd.driftturn > 0 and player.OpenTricks.selectBuffer < 10
      player.OpenTricks.selectBuffer = $ + 1
      if player.OpenTricks.selectBuffer == 1
        OpenTricks_MenuInc(player, -1)
        S_StartSound(nil, sfx_s240, player)
	  end
    elseif player.cmd.driftturn < 0 and player.OpenTricks.selectBuffer < 10
      player.OpenTricks.selectBuffer = $ + 1
      if player.OpenTricks.selectBuffer == 1
        OpenTricks_MenuInc(player, 1)
        S_StartSound(nil, sfx_s240, player)
	  end
    else
      player.OpenTricks.selectBuffer = 0
	  while OpenTricks_Table[player.OpenTricks.trick_number] and OpenTricks_Table[player.OpenTricks.trick_number].banned
	    OpenTricks_MenuInc(player, 1)
	  OpenTricks_MenuClamp(player)
  end
    end
  return
  elseif not player.OpenTricks.selected
    player.OpenTricks.key = OpenTricks_Table[player.OpenTricks.trick_number].name
    S_StartSound(nil, sfx_s258, player)
    player.OpenTricks.selected = true

    if (OpenTricks_Hooks[player.OpenTricks.trick_number].chosenHook) -- execute initialization hook if it exists
      OpenTricks_Hooks[player.OpenTricks.trick_number].chosenHook(player)
    end
    
    player.OpenTricks.combo = 0
  return
  end
  
  local mobj = player.mo
  
  if (OpenTricks_Hooks[player.OpenTricks.trick_number].thinkerHook) -- execute thinker hook if it exists
    OpenTricks_Hooks[player.OpenTricks.trick_number].thinkerHook(player)
  end
  
  if mobj
    if mobj.valid
      if P_IsObjectOnGround(mobj)
        if (OpenTricks_Hooks[player.OpenTricks.trick_number].landingHook) and player.OpenTricks.combo and OpenTricks_CanFinish(player) -- execute landing hook if it exists
          OpenTricks_Hooks[player.OpenTricks.trick_number].landingHook(player)
        end
        player.OpenTricks.combo = 0
      end
  
      local falling_flipped = mobj and mobj.valid and mobj.momz > 0 or ((mobj.flags2 & MF2_OBJECTFLIP) and mobj.momz < 0)
      if (falling_flipped or OpenTricks_CheckMode(otrx_falling)) and not P_IsObjectOnGround(mobj) -- Player is rising, reset the leniency timer.
        player.OpenTricks.leniency = 0
      elseif not OpenTricks_TryingTrick(player) and player.OpenTricks.combo and player.OpenTricks.leniency < TICRATE -- Player is not rising and not tricking, increment the leniency timer.
        player.OpenTricks.leniency = $ + 1
      end
	end
  end
  
  if OpenTricks_CanTrick(player) -- Mobj exists, they're not dying, things like that.
    if not P_IsObjectOnGround(mobj) and player.OpenTricks.leniency < TICRATE -- player is airborne and has been ascending within the last second.
      if OpenTricks_TryingTrick(player) -- player attempting a trick
        if OpenTricks_CanSweetspot(player) and not splitscreen
          S_StartSoundAtVolume(nil, sfx_otrxss, min(otrx_beep.value>>8, 255), player) -- beep sfx for sweetspot assistance, scale beep to a value below 255
        end
      
        if not player.OpenTricks.heldLast -- wasn't tricking last tick
          local margin = max(10, OpenTricks_GetRange(player, true))
          local bigger = max(0 + margin, 32 - margin)
          local smaller = min(0 + margin, 32 - margin)
          if OpenTricks_CheckMode(otrx_random)
            player.OpenTricks.meter.sweetspot = P_RandomRange(smaller, bigger)
            player.OpenTricks.meter.reversed = (P_RandomKey(5) == 1)
          else
            player.OpenTricks.meter.sweetspot = (smaller + bigger)/2
            player.OpenTricks.meter.reversed = false
          end
        else
          player.OpenTricks.meter.timer = min($ + 1, TICRATE)
        end
      else
        if player.OpenTricks.heldLast -- was attempting a trick, now ending it
          if OpenTricks_CanSweetspot(player)
            OpenTricks_Success(player)
          end
          player.OpenTricks.leniency = 0
        end
        player.OpenTricks.meter.timer = 0 -- reset timer.
      end
      player.OpenTricks.heldLast = (player.cmd.buttons & player.OpenTricks.button) -- Record whether they were holding it this tick for future use.
    end
  else
    player.OpenTricks.meter.timer = 0 -- reset timer.
  end
  
  OpenTricks_AnimateTrick(player)
end

local function OpenTricks_BanCommand(player, style, banned)
  if not (player == server or IsPlayerAdmin(player))
    CONS_Printf(player, "Who died and made you Open Tricks god?")
    return
  end
  
  if not style
    CONS_Printf(player, "Clearly you don't own a keyboard.")
    return
  end
  
  for key, value in pairs(OpenTricks_Table_Pending)
    if string.lower(value.name) == string.lower(style)
      OpenTricks_Table_Pending[key].banned = banned
      OpenTricks_TableNeedsUpdate = true
      OpenTricks_Debug("Style \"" .. style .. "\" updated. Changes will be visible during next countdown.")
      return
    end
  end
  OpenTricks_Debug("No style by name \"" .. style .. "\", no changes made.")
end

addHook("ThinkFrame", function()
  for player in players.iterate do
    OpenTricks_Thinker(player)
  end
end)

addHook("ThinkFrame", OpenTricks_Server)

COM_AddCommand(prefix .. "_ban", function(player, style)
  local valid = OpenTricks_GetValidStyles(OpenTricks_Table_Pending)
  if #valid == 0 -- Length
    CONS_Printf(player, "You've banned every style except \"" .. valid[1].name .. "\". Do some unbans first.")
    return
  else
    return OpenTricks_BanCommand(player, style, true)
  end
end)

COM_AddCommand(prefix .. "_unban", function(player, style)
    return OpenTricks_BanCommand(player, style, false)
end)

COM_AddCommand(prefix .. "_button", function(player, input)
  if not input
    OpenTricks_Debug("No input. Try \"brake\", \"c1\", \"c2\", or \"c3\".")
    return
  end
  
  local buttons_table = {
    ["a"] = BT_ACCELERATE, -- are you okay?
    ["acc"] = BT_ACCELERATE,
    ["accel"] = BT_ACCELERATE,
    ["accelerate"] = BT_ACCELERATE,
    ["d"] = BT_DRIFT, -- opentricks player performs worst drift storage ever, asked to leave server
    ["drift"] = BT_DRIFT,
    ["b"] = BT_BRAKE, -- now approaching normalcy.
    ["brake"] = BT_BRAKE,
    ["s"] = BT_BRAKE,
    ["stop"] = BT_BRAKE,
    ["atk"] = BT_ATTACK, -- store users will weep.
    ["attack"] = BT_ATTACK,
    ["i"] = BT_ATTACK,
    ["item"] = BT_ATTACK,
    ["f"] = BT_FORWARD, -- you aren't real.
    ["forward"] = BT_FORWARD,
    ["u"] = BT_FORWARD,
    ["up"] = BT_FORWARD,
    ["back"] = BT_BACKWARD,
    ["backward"] = BT_BACKWARD,
    ["down"] = BT_BACKWARD,
    ["1"] = BT_CUSTOM1, -- acceptable.
    ["c1"] = BT_CUSTOM1,
    ["custom1"] = BT_CUSTOM1,
    ["2"] = BT_CUSTOM2,
    ["c2"] = BT_CUSTOM2,
    ["custom2"] = BT_CUSTOM2,
    ["3"] = BT_CUSTOM3,
    ["c3"] = BT_CUSTOM3,
    ["custom3"] = BT_CUSTOM3
  }
  
  for key, value in pairs(buttons_table)
    if string.lower(key) == string.lower(input)
      player.OpenTricks.button = value
      OpenTricks_Debug("Input changed to \"" .. input .. "\".")
      return
    end
  end
  OpenTricks_Debug("\"" .. input .. "\" doesn't seem to be a valid button option. Try \"brake\", \"c1\", \"c2\", or \"c3\".")
  return
end)