#include "gather.hpp"

#include "../../../resource/font.hpp"
#include "../../../util/imgui.hpp"
#include "../../../util/math.hpp"
#include "../../../util/time.hpp"

#include <algorithm>
#include <cmath>
#include <format>
#include <imgui.h>

namespace game::state::play::menu
{
  constexpr auto CANVAS_MIN_SIZE = 1.0f;
  constexpr auto CENTER_RATIO = 0.5f;
  constexpr auto START_TEXT_Y_RATIO = 0.75f;
  constexpr auto PLAYER_ACCELERATION = 3.0f;
  constexpr auto PLAYER_FRICTION_PER_SECOND = 0.72f;
  constexpr auto PLAYER_SIZE = 32.0f;
  constexpr auto PLAYER_VELOCITY_MAX = 720.0f;
  constexpr auto PLAYER_VELOCITY_STOP = 0.0f;
  constexpr auto PLAYER_BOOST_VELOCITY_GAIN = 320.0f;
  constexpr auto PLAYER_BOOST_VELOCITY_MIN = 540.0f;
  constexpr auto PLAYER_BOOST_DURATION = 0.35f;
  constexpr auto BOOST_DIRECTION_EPSILON = 0.001f;
  constexpr auto BOOST_DIRECTION_UNIT = 1.0f;
  constexpr auto GAME_TEXT_FONT_SIZE = resource::Font::HEADER_1;
  constexpr auto STAMINA_MIN = 0.0f;
  constexpr auto STAMINA_MAX = 1.0f;
  constexpr auto STAMINA_REGEN_PER_TICK = 0.002f;
  constexpr auto STAMINA_BOOST_COST = 0.25f;
  constexpr auto STAMINA_MAGNET_COST = 0.25f;
  constexpr auto STAMINA_BAR_WIDTH = 120.0f;
  constexpr auto STAMINA_BAR_HEIGHT = GAME_TEXT_FONT_SIZE;
  constexpr auto STAMINA_BAR_PADDING = 8.0f;
  constexpr auto STAMINA_BAR_PADDING_MULTIPLIER = 2.0f;
  constexpr auto STAMINA_BAR_ROUNDING = 2.0f;
  constexpr auto COLLECTIBLE_SIZE = 20.0f;
  constexpr auto COLLECTIBLE_OSCILLATION_SPEED = 6.0f;
  constexpr auto COLLECTIBLE_OSCILLATION_AMOUNT = 3.0f;
  constexpr auto COLLECTIBLE_MAGNET_RADIUS = 72.0f;
  constexpr auto COLLECTIBLE_MAGNET_RADIUS_EXTENDED = 240.0f;
  constexpr auto COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS = 1.0f;
  constexpr auto COLLECTIBLE_MAGNET_RADIUS_WINDOWED_MULTIPLIER = 0.5f;
  constexpr auto COLLECTIBLE_MAGNET_ACCELERATION = 5200.0f;
  constexpr auto COLLECTIBLE_MAGNET_ACCELERATION_GAIN = 26000.0f;
  constexpr auto COLLECTIBLE_MAGNET_ACCELERATION_MAX = 26000.0f;
  constexpr auto COLLECTIBLE_MAGNET_VELOCITY_MIN = 900.0f;
  constexpr auto COLLECTIBLE_MAGNET_SPEED_MAX = 1800.0f;
  constexpr auto COLLECTIBLE_MAGNET_FRICTION_PER_SECOND = 0.98f;
  constexpr auto COLLECTIBLE_MAGNET_SIDE_VELOCITY_KEEP = 0.35f;
  constexpr auto COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER = 2.0f;
  constexpr auto COLLECTIBLE_STRUCTURE_SPACING = 34.0f;
  constexpr auto COLLECTIBLE_STRUCTURE_COUNT_MIN = 3;
  constexpr auto COLLECTIBLE_POLYGON_RADIUS_BASE = 28.0f;
  constexpr auto COLLECTIBLE_POLYGON_RADIUS_PER_COLLECTIBLE = 4.5f;
  constexpr auto STAMINA_PICKUP_COUNT = 1;
  constexpr auto COLLECTIBLE_TWO_PI = 6.283185f;
  constexpr auto COLLECTIBLE_POLYGON_CHANCE = 0.20f;
  constexpr auto COLLECTIBLE_LINE_CHANCE = 0.55f;
  constexpr auto SCORE_TEXT_PADDING = 8.0f;
  constexpr auto SCORE_TEXT_LINE_MULTIPLIER = 1.15f;
  constexpr auto GAME_TIME_START_SECONDS = 30.0f;
  constexpr auto GAME_TIME_START_TICKS = GAME_TIME_START_SECONDS * Entity::UPDATE_RATE;
  constexpr auto GAME_TIME_GAIN_SECONDS = 5.0f;
  constexpr auto GAME_TIME_GAIN_TICKS = GAME_TIME_GAIN_SECONDS * Entity::UPDATE_RATE;
  constexpr auto WAVE_RESET_FADE_SECONDS = 0.125f;
  constexpr auto TIME_GAIN_ALPHA_MAX = 1.0f;
  constexpr auto TIME_GAIN_FADE_SECONDS = 1.25f;
  constexpr auto TIME_TEXT_PADDING = 8.0f;
  constexpr auto COLOR_CHANNEL_MAX = 255;
  constexpr auto COLOR_GREEN_DIM = 64;
  constexpr auto COLOR_GREEN = 220;
  constexpr auto COLOR_COLLECTIBLE_BLUE = 255;
  constexpr auto COLOR_TIME = 255;
  constexpr auto COLOR_MAGNET_BLUE = 255;
  constexpr auto COLOR_MAGNET_ALPHA = 70;
  constexpr auto MAGNET_CIRCLE_SEGMENTS = 96;
  constexpr auto MAGNET_CIRCLE_THICKNESS = 2.0f;
  constexpr auto COLOR_COLLECTIBLE_ALPHA = 255;
  constexpr auto COLOR_COLLECTIBLE_NEXT_ALPHA = 35;
  constexpr auto STYLE_VAR_COUNT = 1;
  constexpr auto CANVAS_FLAGS = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;

  float Gather::random_range_get(float min, float max)
  {
    if (min >= max) return min;
    return util::math::random_in_range(min, max);
  }

  glm::vec2 Gather::random_position_get(float minX, float minY, float maxX, float maxY)
  {
    return {random_range_get(minX, maxX), random_range_get(minY, maxY)};
  }

  Entity Gather::collectible_make(glm::vec2 position)
  {
    Entity collectible{};
    collectible.position = position;
    collectible.phase = util::math::random_max(COLLECTIBLE_TWO_PI);
    return collectible;
  }

  Gather::Structure Gather::structure_get(int remaining)
  {
    auto roll = util::math::random();
    if (remaining < COLLECTIBLE_STRUCTURE_COUNT_MIN) return Structure::SINGLE;
    if (roll < COLLECTIBLE_POLYGON_CHANCE) return Structure::POLYGON;
    if (roll < COLLECTIBLE_LINE_CHANCE)
      return util::math::random_bool() ? Structure::LINE_HORIZONTAL : Structure::LINE_VERTICAL;
    return Structure::SINGLE;
  }

  int Gather::structure_count_get(int remaining)
  {
    auto countRange = (remaining - COLLECTIBLE_STRUCTURE_COUNT_MIN) + 1;
    auto countRoll = std::min(countRange - 1, (int)util::math::random_max((float)countRange));
    return COLLECTIBLE_STRUCTURE_COUNT_MIN + countRoll;
  }

  int Gather::wave_collectible_count_get(int wave) { return std::max(1, wave); }

  void Gather::collectible_add(std::vector<Entity>& wave, glm::vec2 position, int targetCount)
  {
    if ((int)wave.size() >= targetCount) return;
    wave.emplace_back(collectible_make(position));
  }

  void Gather::single_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, int targetCount)
  {
    auto position = random_position_get(margin, margin, canvasSize.x - margin, canvasSize.y - margin);
    collectible_add(wave, position, targetCount);
  }

  void Gather::line_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, bool isHorizontal,
                          int structureCount, int targetCount)
  {
    auto structureHalf = COLLECTIBLE_STRUCTURE_SPACING * ((float)structureCount - 1) * CENTER_RATIO;
    auto clearance = structureHalf + (COLLECTIBLE_SIZE * CENTER_RATIO);
    auto center =
        isHorizontal
            ? random_position_get(margin + clearance, margin, canvasSize.x - margin - clearance, canvasSize.y - margin)
            : random_position_get(margin, margin + clearance, canvasSize.x - margin, canvasSize.y - margin - clearance);
    for (auto i = 0; i < structureCount; ++i)
    {
      auto offset = ((float)i - (((float)structureCount - 1) * CENTER_RATIO)) * COLLECTIBLE_STRUCTURE_SPACING;
      auto position = isHorizontal ? glm::vec2(center.x + offset, center.y) : glm::vec2(center.x, center.y + offset);
      collectible_add(wave, position, targetCount);
    }
  }

  float Gather::polygon_radius_get(int structureCount)
  {
    return COLLECTIBLE_POLYGON_RADIUS_BASE + ((float)structureCount * COLLECTIBLE_POLYGON_RADIUS_PER_COLLECTIBLE);
  }

  void Gather::polygon_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, int structureCount,
                             int targetCount)
  {
    auto radius = polygon_radius_get(structureCount);
    auto clearance = radius + (COLLECTIBLE_SIZE * CENTER_RATIO);
    auto center = random_position_get(margin + clearance, margin + clearance, canvasSize.x - margin - clearance,
                                      canvasSize.y - margin - clearance);
    for (auto i = 0; i < structureCount; ++i)
    {
      auto angle = COLLECTIBLE_TWO_PI * ((float)i / (float)structureCount);
      auto position = center + glm::vec2(std::cos(angle), std::sin(angle)) * radius;
      collectible_add(wave, position, targetCount);
    }
  }

  void Gather::wave_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, int waveNumber)
  {
    wave.clear();
    auto targetCount = wave_collectible_count_get(waveNumber);
    auto margin = PLAYER_SIZE * COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER;
    while ((int)wave.size() < targetCount)
    {
      auto remaining = targetCount - (int)wave.size();
      switch (structure_get(remaining))
      {
        case Structure::LINE_HORIZONTAL:
          line_spawn(wave, canvasSize, margin, true, structure_count_get(remaining), targetCount);
          break;
        case Structure::LINE_VERTICAL:
          line_spawn(wave, canvasSize, margin, false, structure_count_get(remaining), targetCount);
          break;
        case Structure::POLYGON:
          polygon_spawn(wave, canvasSize, margin, structure_count_get(remaining), targetCount);
          break;
        case Structure::SINGLE:
        default:
          single_spawn(wave, canvasSize, margin, targetCount);
          break;
      }
    }
  }

  int Gather::stamina_pickup_count_get(int) { return STAMINA_PICKUP_COUNT; }

  void Gather::stamina_pickups_spawn(std::vector<Entity>& pickups, ImVec2 canvasSize, int waveNumber)
  {
    pickups.clear();
    auto targetCount = stamina_pickup_count_get(waveNumber);
    auto margin = PLAYER_SIZE * COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER;
    while ((int)pickups.size() < targetCount)
      single_spawn(pickups, canvasSize, margin, targetCount);
  }

  float Gather::wave_time_gain_ticks_get() { return GAME_TIME_GAIN_TICKS; }

  bool Gather::collectible_hit_get(const Entity& collectible, const Entity& player)
  {
    auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
    auto collectibleHalf = COLLECTIBLE_SIZE * CENTER_RATIO;
    auto distance = glm::abs(player.position - collectible.position);
    return distance.x <= playerHalf + collectibleHalf && distance.y <= playerHalf + collectibleHalf;
  }

  float Gather::magnet_radius_get(float magnetRadiusTime, bool isMenuFullscreen)
  {
    auto multiplier = isMenuFullscreen ? STAMINA_MAX : COLLECTIBLE_MAGNET_RADIUS_WINDOWED_MULTIPLIER;
    auto baseRadius = COLLECTIBLE_MAGNET_RADIUS * multiplier;
    auto extendedRadius = COLLECTIBLE_MAGNET_RADIUS_EXTENDED * multiplier;
    auto ratio = std::clamp(magnetRadiusTime / COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS, STAMINA_MIN, STAMINA_MAX);
    return baseRadius + ((extendedRadius - baseRadius) * ratio);
  }

  int Gather::collectibles_collect(std::vector<Entity>& wave, const Entity& player)
  {
    auto collected = 0;
    for (auto& collectible : wave)
    {
      if (collectible.isCollected || !collectible_hit_get(collectible, player)) continue;
      collectible.isCollected = true;
      ++collected;
    }
    return collected;
  }

  void Gather::collectibles_magnet_update(std::vector<Entity>& wave, const Entity& player, ImVec2 canvasSize,
                                          float magnetRadius, float dt)
  {
    auto radiusSquared = magnetRadius * magnetRadius;
    auto collectibleHalf = COLLECTIBLE_SIZE * CENTER_RATIO;
    auto minX = collectibleHalf;
    auto minY = collectibleHalf;
    auto maxX = std::max(minX, canvasSize.x - collectibleHalf);
    auto maxY = std::max(minY, canvasSize.y - collectibleHalf);
    for (auto& collectible : wave)
    {
      if (collectible.isCollected) continue;
      auto direction = player.position - collectible.position;
      auto distanceSquared = (direction.x * direction.x) + (direction.y * direction.y);
      if (distanceSquared <= radiusSquared) collectible.isMagnetized = true;
      if (collectible.isMagnetized && distanceSquared > BOOST_DIRECTION_EPSILON)
      {
        collectible.magnetTime += dt;
        auto magnetAcceleration =
            std::min(COLLECTIBLE_MAGNET_ACCELERATION_MAX,
                     COLLECTIBLE_MAGNET_ACCELERATION + (COLLECTIBLE_MAGNET_ACCELERATION_GAIN * collectible.magnetTime));
        auto distance = std::sqrt(distanceSquared);
        auto directionUnit = direction * (BOOST_DIRECTION_UNIT / distance);
        auto inwardVelocity = (collectible.velocity.x * directionUnit.x) + (collectible.velocity.y * directionUnit.y);
        auto inward = directionUnit * inwardVelocity;
        auto sideways = collectible.velocity - inward;
        collectible.velocity = inward + (sideways * COLLECTIBLE_MAGNET_SIDE_VELOCITY_KEEP);
        collectible.velocity += directionUnit * magnetAcceleration * dt;
        inwardVelocity = (collectible.velocity.x * directionUnit.x) + (collectible.velocity.y * directionUnit.y);
        if (inwardVelocity < COLLECTIBLE_MAGNET_VELOCITY_MIN)
          collectible.velocity += directionUnit * (COLLECTIBLE_MAGNET_VELOCITY_MIN - inwardVelocity);
      }

      auto velocityLength = std::sqrt((collectible.velocity.x * collectible.velocity.x) +
                                      (collectible.velocity.y * collectible.velocity.y));
      if (velocityLength > COLLECTIBLE_MAGNET_SPEED_MAX)
      {
        auto velocityScale = COLLECTIBLE_MAGNET_SPEED_MAX / velocityLength;
        collectible.velocity *= velocityScale;
      }

      auto velocityRetention = std::pow(COLLECTIBLE_MAGNET_FRICTION_PER_SECOND, dt);
      collectible.velocity *= velocityRetention;
      collectible.position += collectible.velocity * dt;
      if (collectible.position.x < minX || collectible.position.x > maxX) collectible.velocity.x = PLAYER_VELOCITY_STOP;
      if (collectible.position.y < minY || collectible.position.y > maxY) collectible.velocity.y = PLAYER_VELOCITY_STOP;
      collectible.position.x = std::clamp(collectible.position.x, minX, maxX);
      collectible.position.y = std::clamp(collectible.position.y, minY, maxY);
    }
  }

  bool Gather::wave_complete_get(const std::vector<Entity>& wave)
  {
    if (wave.empty()) return false;
    for (auto& collectible : wave)
      if (!collectible.isCollected) return false;
    return true;
  }

  void Gather::collectibles_draw(const std::vector<Entity>& wave, ImDrawList* drawList, ImVec2 canvasMin, float time,
                                 ImU32 color)
  {
    for (auto& collectible : wave)
    {
      if (collectible.isCollected) continue;
      auto size = COLLECTIBLE_SIZE + (std::sin((time * COLLECTIBLE_OSCILLATION_SPEED) + collectible.phase) *
                                      COLLECTIBLE_OSCILLATION_AMOUNT);
      auto half = size * CENTER_RATIO;
      auto min = ImVec2(canvasMin.x + collectible.position.x - half, canvasMin.y + collectible.position.y - half);
      auto max = ImVec2(min.x + size, min.y + size);
      drawList->AddRectFilled(min, max, color);
    }
  }

  void Gather::magnet_radius_draw(ImDrawList* drawList, ImVec2 canvasMin, const Entity& player, float radius)
  {
    auto center = ImVec2(canvasMin.x + player.position.x, canvasMin.y + player.position.y);
    drawList->AddCircle(center, radius, IM_COL32(0, 0, COLOR_MAGNET_BLUE, COLOR_MAGNET_ALPHA), MAGNET_CIRCLE_SEGMENTS,
                        MAGNET_CIRCLE_THICKNESS);
  }

  int Gather::alpha_get(int alpha, float multiplier)
  {
    return (int)((float)alpha * std::clamp(multiplier, STAMINA_MIN, STAMINA_MAX));
  }

  void Gather::progress_bar_draw(ImDrawList* drawList, ImVec2 min, ImVec2 max, float progress, ImU32 backgroundColor,
                                 ImU32 fillColor, const std::string& text)
  {
    auto fillMax = ImVec2(min.x + ((max.x - min.x) * std::clamp(progress, STAMINA_MIN, STAMINA_MAX)), max.y);
    drawList->AddRectFilled(min, max, backgroundColor, STAMINA_BAR_ROUNDING);
    drawList->AddRectFilled(min, fillMax, fillColor, STAMINA_BAR_ROUNDING);
    if (text.empty()) return;

    auto textSize = util::imgui::draw_list_text_size_get(text, GAME_TEXT_FONT_SIZE);
    auto textPos = ImVec2(min.x + ((max.x - min.x - textSize.x) * CENTER_RATIO),
                          min.y + ((max.y - min.y - textSize.y) * CENTER_RATIO));
    util::imgui::draw_list_text_draw(
        *drawList, textPos, text, GAME_TEXT_FONT_SIZE,
        IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
  }

  void Gather::reset()
  {
    player = {};
    collectibles.clear();
    nextCollectibles.clear();
    staminaPickups.clear();
    nextStaminaPickups.clear();
    targetPosition = {};
    stamina = STAMINA_MAX;
    boostTime = STAMINA_MIN;
    magnetRadiusTime = STAMINA_MIN;
    timeTicks = GAME_TIME_START_TICKS;
    timeGainTicks = STAMINA_MIN;
    timeGainAlpha = STAMINA_MIN;
    score = 0;
    wave = 1;
    isStarted = false;
    isPaused = false;
    isMenuFullscreenKnown = false;
    isMenuFullscreenLast = false;
    isWaveResetPending = false;
    waveResetAlpha = STAMINA_MAX;
  }

  void Gather::start(ImVec2 canvasSize)
  {
    reset();
    player.position = glm::vec2(canvasSize.x * CENTER_RATIO, canvasSize.y * CENTER_RATIO);
    targetPosition = player.position;
    isStarted = true;
    wave_reset(canvasSize);
  }

  void Gather::wave_reset(ImVec2 canvasSize)
  {
    wave_spawn(collectibles, canvasSize, wave);
    wave_spawn(nextCollectibles, canvasSize, wave + 1);
    stamina_pickups_spawn(staminaPickups, canvasSize, wave);
    stamina_pickups_spawn(nextStaminaPickups, canvasSize, wave + 1);
    magnetRadiusTime = STAMINA_MIN;
    isWaveResetPending = false;
    waveResetAlpha = STAMINA_MAX;
  }

  void Gather::wave_reset_request()
  {
    isWaveResetPending = true;
    waveResetAlpha = STAMINA_MAX;
    magnetRadiusTime = STAMINA_MIN;
  }

  void Gather::score_draw(ImDrawList* drawList, ImVec2 canvasMin, int score, int wave)
  {
    auto scoreText = std::format("{}", score);
    auto position = ImVec2(canvasMin.x + SCORE_TEXT_PADDING, canvasMin.y + SCORE_TEXT_PADDING);
    util::imgui::draw_list_text_draw(
        *drawList, position, scoreText, GAME_TEXT_FONT_SIZE,
        IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
    auto waveText = std::format("Wave #{}", wave);
    auto wavePosition = ImVec2(position.x, position.y + (GAME_TEXT_FONT_SIZE * SCORE_TEXT_LINE_MULTIPLIER));
    util::imgui::draw_list_text_draw(
        *drawList, wavePosition, waveText, GAME_TEXT_FONT_SIZE,
        IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
  }

  void Gather::time_draw(ImDrawList* drawList, ImVec2 canvasMin, ImVec2 canvasMax, float timeTicks, float timeGainTicks,
                         float timeGainAlpha)
  {
    auto timeText = util::time::ticks_to_minutes_seconds_centiseconds((int)std::ceil(timeTicks));
    auto textSize = util::imgui::draw_list_text_size_get(timeText, GAME_TEXT_FONT_SIZE);
    auto textWidth = util::imgui::draw_list_digit_fixed_width_get(timeText, GAME_TEXT_FONT_SIZE);
    auto textPos = ImVec2(canvasMax.x - textWidth - TIME_TEXT_PADDING, canvasMin.y + TIME_TEXT_PADDING);
    util::imgui::draw_list_digit_fixed_width_draw(*drawList, textPos, timeText, GAME_TEXT_FONT_SIZE,
                                                  IM_COL32(COLOR_TIME, COLOR_TIME, COLOR_TIME, COLOR_CHANNEL_MAX));

    if (timeGainAlpha <= STAMINA_MIN) return;
    auto gainText =
        std::format("+{}", util::time::ticks_to_minutes_seconds_centiseconds((int)std::ceil(timeGainTicks)));
    auto alpha = (int)(COLOR_CHANNEL_MAX * std::clamp(timeGainAlpha, STAMINA_MIN, STAMINA_MAX));
    auto gainTextWidth = util::imgui::draw_list_digit_fixed_width_get(gainText, GAME_TEXT_FONT_SIZE);
    auto gainPos = ImVec2(canvasMax.x - gainTextWidth - TIME_TEXT_PADDING, textPos.y + textSize.y + TIME_TEXT_PADDING);
    util::imgui::draw_list_digit_fixed_width_draw(*drawList, gainPos, gainText, GAME_TEXT_FONT_SIZE,
                                                  IM_COL32(COLOR_TIME, COLOR_TIME, COLOR_TIME, alpha));
  }

  void Gather::update(bool isMenuFullscreen, bool isMenuResizeSettled)
  {
    auto canvasSize = ImGui::GetContentRegionAvail();
    if (canvasSize.x <= CANVAS_MIN_SIZE || canvasSize.y <= CANVAS_MIN_SIZE) return;

    ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
    if (ImGui::BeginChild("##Gather Canvas Child", canvasSize, ImGuiChildFlags_None, CANVAS_FLAGS))
    {
      canvasSize = ImGui::GetContentRegionAvail();
      auto canvasMin = ImGui::GetCursorScreenPos();
      auto canvasMax = ImVec2(canvasMin.x + canvasSize.x, canvasMin.y + canvasSize.y);
      auto drawList = ImGui::GetWindowDrawList();
      auto dt = ImGui::GetIO().DeltaTime;
      auto isCanvasFocused = ImGui::IsWindowFocused();

      ImGui::InvisibleButton("##Gather Canvas", canvasSize);
      auto isCanvasHovered = ImGui::IsItemHovered();

      drawList->AddRectFilled(canvasMin, canvasMax, IM_COL32(0, 0, 0, COLOR_CHANNEL_MAX / 2));

      if (!isMenuFullscreenKnown)
      {
        isMenuFullscreenKnown = true;
        isMenuFullscreenLast = isMenuFullscreen;
      }
      auto isMenuFullscreenChanged = isMenuFullscreenLast != isMenuFullscreen;
      isMenuFullscreenLast = isMenuFullscreen;

      if (isStarted && isMenuFullscreenChanged) wave_reset_request();

      if (isStarted && isWaveResetPending)
      {
        waveResetAlpha = std::max(STAMINA_MIN, waveResetAlpha - (dt / WAVE_RESET_FADE_SECONDS));
        if (isMenuResizeSettled) wave_reset(canvasSize);
      }

      if (!isStarted && isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) start(canvasSize);

      if (isStarted)
      {
        if (isCanvasFocused && ImGui::IsKeyPressed(ImGuiKey_P, false)) isPaused = !isPaused;

        auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
        auto minX = std::min(playerHalf, canvasSize.x * CENTER_RATIO);
        auto minY = std::min(playerHalf, canvasSize.y * CENTER_RATIO);
        auto maxX = std::max(minX, canvasSize.x - playerHalf);
        auto maxY = std::max(minY, canvasSize.y - playerHalf);

        player.position.x = std::clamp(player.position.x, minX, maxX);
        player.position.y = std::clamp(player.position.y, minY, maxY);
        targetPosition.x = std::clamp(targetPosition.x, minX, maxX);
        targetPosition.y = std::clamp(targetPosition.y, minY, maxY);

        if (!isPaused)
        {
          if (isCanvasHovered)
          {
            auto mouse = ImGui::GetIO().MousePos;
            targetPosition =
                glm::vec2(std::clamp(mouse.x - canvasMin.x, minX, maxX), std::clamp(mouse.y - canvasMin.y, minY, maxY));
          }

          timeTicks = std::max(STAMINA_MIN, timeTicks - (Entity::UPDATE_RATE * dt));
          if (timeTicks <= STAMINA_MIN) reset();

          if (isStarted)
          {
            timeGainAlpha = std::max(STAMINA_MIN, timeGainAlpha - (dt / TIME_GAIN_FADE_SECONDS));
            stamina =
                std::clamp(stamina + (STAMINA_REGEN_PER_TICK * Entity::UPDATE_RATE * dt), STAMINA_MIN, STAMINA_MAX);
            if (isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle) && stamina >= STAMINA_MAGNET_COST)
            {
              magnetRadiusTime = COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS;
              stamina = std::max(STAMINA_MIN, stamina - STAMINA_MAGNET_COST);
            }
            if (isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && stamina >= STAMINA_BOOST_COST)
            {
              auto boostDirection = targetPosition - player.position;
              auto boostDirectionLength =
                  std::sqrt((boostDirection.x * boostDirection.x) + (boostDirection.y * boostDirection.y));
              if (boostDirectionLength > BOOST_DIRECTION_EPSILON)
              {
                auto boostDirectionScale = BOOST_DIRECTION_UNIT / boostDirectionLength;
                auto boostDirectionUnit = boostDirection * boostDirectionScale;
                player.velocity.x += boostDirectionUnit.x * PLAYER_BOOST_VELOCITY_GAIN;
                player.velocity.y += boostDirectionUnit.y * PLAYER_BOOST_VELOCITY_GAIN;
                auto boostVelocity =
                    (player.velocity.x * boostDirectionUnit.x) + (player.velocity.y * boostDirectionUnit.y);
                if (boostVelocity < PLAYER_BOOST_VELOCITY_MIN)
                {
                  auto boostVelocityAdd = PLAYER_BOOST_VELOCITY_MIN - boostVelocity;
                  player.velocity.x += boostDirectionUnit.x * boostVelocityAdd;
                  player.velocity.y += boostDirectionUnit.y * boostVelocityAdd;
                }
                boostTime = PLAYER_BOOST_DURATION;
                stamina = std::max(STAMINA_MIN, stamina - STAMINA_BOOST_COST);
              }
            }

            player.velocity.x += (targetPosition.x - player.position.x) * PLAYER_ACCELERATION * dt;
            player.velocity.y += (targetPosition.y - player.position.y) * PLAYER_ACCELERATION * dt;
            auto velocityRetention = std::pow(PLAYER_FRICTION_PER_SECOND, dt);
            player.velocity.x *= velocityRetention;
            player.velocity.y *= velocityRetention;
            auto velocityLength =
                std::sqrt((player.velocity.x * player.velocity.x) + (player.velocity.y * player.velocity.y));
            auto isBoostActive = boostTime > STAMINA_MIN;
            if (!isBoostActive && velocityLength > PLAYER_VELOCITY_MAX)
            {
              auto velocityScale = PLAYER_VELOCITY_MAX / velocityLength;
              player.velocity.x *= velocityScale;
              player.velocity.y *= velocityScale;
            }
            boostTime = std::max(STAMINA_MIN, boostTime - dt);
            player.position.x += player.velocity.x * dt;
            player.position.y += player.velocity.y * dt;

            if (player.position.x < minX || player.position.x > maxX) player.velocity.x = PLAYER_VELOCITY_STOP;
            if (player.position.y < minY || player.position.y > maxY) player.velocity.y = PLAYER_VELOCITY_STOP;
            player.position.x = std::clamp(player.position.x, minX, maxX);
            player.position.y = std::clamp(player.position.y, minY, maxY);

            if (!isWaveResetPending)
            {
              auto magnetRadius = magnet_radius_get(magnetRadiusTime, isMenuFullscreen);
              collectibles_magnet_update(collectibles, player, canvasSize, magnetRadius, dt);
              magnetRadiusTime = std::max(STAMINA_MIN, magnetRadiusTime - dt);
              score += collectibles_collect(collectibles, player);
              if (collectibles_collect(staminaPickups, player) > 0) stamina = STAMINA_MAX;
              if (wave_complete_get(collectibles))
              {
                timeGainTicks = wave_time_gain_ticks_get();
                timeTicks += timeGainTicks;
                timeGainAlpha = TIME_GAIN_ALPHA_MAX;
                ++wave;
                collectibles = nextCollectibles;
                staminaPickups = nextStaminaPickups;
                wave_spawn(nextCollectibles, canvasSize, wave + 1);
                stamina_pickups_spawn(nextStaminaPickups, canvasSize, wave + 1);
              }
            }
          }
        }
      }

      if (isStarted)
      {
        auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
        auto collectibleAlpha = alpha_get(COLOR_COLLECTIBLE_ALPHA, waveResetAlpha);
        auto nextCollectibleAlpha = alpha_get(COLOR_COLLECTIBLE_NEXT_ALPHA, waveResetAlpha);
        collectibles_draw(nextCollectibles, drawList, canvasMin, (float)ImGui::GetTime(),
                          IM_COL32(0, 0, COLOR_COLLECTIBLE_BLUE, nextCollectibleAlpha));
        collectibles_draw(nextStaminaPickups, drawList, canvasMin, (float)ImGui::GetTime(),
                          IM_COL32(0, COLOR_GREEN, 0, nextCollectibleAlpha));
        collectibles_draw(collectibles, drawList, canvasMin, (float)ImGui::GetTime(),
                          IM_COL32(0, 0, COLOR_COLLECTIBLE_BLUE, collectibleAlpha));
        collectibles_draw(staminaPickups, drawList, canvasMin, (float)ImGui::GetTime(),
                          IM_COL32(0, COLOR_GREEN, 0, collectibleAlpha));
        magnet_radius_draw(drawList, canvasMin, player, magnet_radius_get(magnetRadiusTime, isMenuFullscreen));

        auto playerMin =
            ImVec2(canvasMin.x + player.position.x - playerHalf, canvasMin.y + player.position.y - playerHalf);
        auto playerMax = ImVec2(playerMin.x + PLAYER_SIZE, playerMin.y + PLAYER_SIZE);
        drawList->AddRectFilled(playerMin, playerMax,
                                IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));

        auto staminaBarWidth =
            std::min(STAMINA_BAR_WIDTH,
                     std::max(CANVAS_MIN_SIZE, canvasSize.x - (STAMINA_BAR_PADDING * STAMINA_BAR_PADDING_MULTIPLIER)));
        auto staminaBarMin = ImVec2(canvasMax.x - staminaBarWidth - STAMINA_BAR_PADDING,
                                    canvasMax.y - STAMINA_BAR_HEIGHT - STAMINA_BAR_PADDING);
        auto staminaBarMax = ImVec2(staminaBarMin.x + staminaBarWidth, staminaBarMin.y + STAMINA_BAR_HEIGHT);
        progress_bar_draw(drawList, staminaBarMin, staminaBarMax, stamina,
                          IM_COL32(0, COLOR_GREEN_DIM, 0, COLOR_CHANNEL_MAX),
                          IM_COL32(0, COLOR_GREEN, 0, COLOR_CHANNEL_MAX));
        score_draw(drawList, canvasMin, score, wave);
        time_draw(drawList, canvasMin, canvasMax, timeTicks, timeGainTicks, timeGainAlpha);
        if (isPaused)
          util::imgui::draw_list_text_center_draw(
              *drawList, canvasMin, canvasSize, "PAUSED", GAME_TEXT_FONT_SIZE, CENTER_RATIO, CENTER_RATIO,
              IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
      }
      else
        util::imgui::draw_list_text_center_draw(
            *drawList, canvasMin, canvasSize, "Click to start!", GAME_TEXT_FONT_SIZE, CENTER_RATIO, START_TEXT_Y_RATIO,
            IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
    }
    ImGui::EndChild();
    ImGui::PopStyleVar(STYLE_VAR_COUNT);
  }
}
