local otricks_range = CV_RegisterVar({
  name = "otricks_range",
  defaultvalue = "7",
  possiblevalue = {MIN = 0, MAX = 16},
  flags = CV_NETVAR
})

local otricks_spb_range = CV_RegisterVar({
  name = "otricks_spb_range",
  defaultvalue = "2",
  possiblevalue = {MIN = 0, MAX = 16},
  flags = CV_NETVAR
})

local otricks_beep = CV_RegisterVar({
  name = "otricks_beep",
  defaultvalue = "200",
  possiblevalue = {MIN = 0, MAX = 255}
})

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 otricks_random = CV_RegisterVar({
  name = "otricks_random",
  defaultvalue = "Race",
  possiblevalue = modes_table,
  flags = CV_NETVAR
})

local otricks_falling = CV_RegisterVar({
  name = "otricks_falling",
  defaultvalue = "Encore",
  possiblevalue = modes_table,
  flags = CV_NETVAR
})

rawset(_G, "OpenTricks_Table", {})
rawset(_G, "OpenTricks_Table_Pending", {})
rawset(_G, "OpenTricks_Hooks", {})
local OpenTricksTableNeedsUpdate = 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_Add(name, hint, offIcon, onIcon, defaultBanned, chosenHook, thinkerHook, successHook, landingHook)
  -- name: string used as a key for stuff specific to your Trick Style, and for UI
  -- hint: string used for selection UI.
  -- icon: patch name for the associated graphic.
  
  -- All hooks use the player entity as the sole argument.
  -- chosenHook:  function called when the player's choice is locked in. Initialize stuff here.
  -- thinkerHook: function called every tick for each player using this Trick Style. Activates before other hooks.
  -- successHook: function called for each time a player succeeds (mid-air only) while using this Trick Style.
  --              Use player.OpenTricks.combo to get the number of tricks so far (including this one.)
  -- landingHook: function that gets called for each time a player lands while using this Trick Style.
  --              Use player.OpenTricks.combo to get the number of tricks prior to landing.
  
  local new_trick = {}
  new_trick.name = name
  new_trick.hint = hint
  new_trick.offIcon = offIcon
  new_trick.onIcon = onIcon
  new_trick.banned = defaultBanned
  
  local new_hooks = {}
  new_hooks.name = name
  new_hooks.chosenHook = chosenHook
  new_hooks.thinkerHook = thinkerHook
  new_hooks.successHook = successHook
  new_hooks.landingHook = landingHook
  
  if #OpenTricks_Table_Pending == #OpenTricks_Hooks -- check to see if we've gained non-hooked trick styles from netplay.
    table.insert(OpenTricks_Table_Pending, new_trick) 
  end
  table.insert(OpenTricks_Hooks, new_hooks)
  OpenTricksTableNeedsUpdate = true
  
  OpenTricks_Debug(name .. " registered.")
end

local function OpenTricks_GetRange(player, check_spb)
  if (check_spb and spbplace == player.kartstuff[k_position])
    return otricks_spb_range.value
  end
  return otricks_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
  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 OpenTricksTableNeedsUpdate
    OpenTricks_Table = OpenTricks_GetValidStyles(OpenTricks_Table_Pending)
    OpenTricksTableNeedsUpdate = 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.frameangle = 0
    player.OpenTricks.tricking = 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
end

local function OpenTricks_Thinker(player)
  if not OpenTricks_Table
    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}}
    player.trickactive = 0
  end
  
  if (otricks_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))
      player.OpenTricks.selectBuffer = 0 -- no.
    elseif player.cmd.driftturn > 0 and player.OpenTricks.selectBuffer < 10
      player.OpenTricks.selectBuffer = $ + 1
      if player.OpenTricks.selectBuffer == 1
        player.OpenTricks.trick_number = $ - 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
        player.OpenTricks.trick_number = $ + 1
        S_StartSound(nil, sfx_s240, player)
      end
    else
      player.OpenTricks.selectBuffer = 0
    end
    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
  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 and mobj.valid and 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(otricks_falling)) and not P_IsObjectOnGround(mobj) -- Player is rising, reset the leniency timer.
    player.OpenTricks.leniency = 0
  elseif not player.OpenTricks.combo and player.OpenTricks.leniency < TICRATE -- Player is not rising and not tricking, increment the leniency timer.
    player.OpenTricks.leniency = $ + 1
  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, otricks_beep.value, player) -- beep sfx for sweetspot assistance
		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(otricks_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 style == nil or not (player == server or IsPlayerAdmin(player))
    CONS_Printf(player, "Who died and made you Open Tricks god?")
    return
  end
  
  for key, value in pairs(OpenTricks_Table_Pending)
    if string.lower(value.name) == string.lower(style)
      OpenTricks_Table_Pending[key].banned = banned
      OpenTricksTableNeedsUpdate = 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

rawset(_G, "OPENTRICKS_ADD", OpenTricks_Add)

addHook("PlayerThink", OpenTricks_Thinker)

addHook("ThinkFrame", OpenTricks_Server)

COM_AddCommand("otricks_ban", function(player, style)
  local valid = OpenTricks_GetValidStyles(OpenTricks_Table_Pending)
  if #valid == 1
    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("otricks_unban", function(player, style)
	return OpenTricks_BanCommand(player, style, false)
end)

COM_AddCommand("otricks_button", function(player, input)
  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)