#include "subway_map.h"

#include <fmt/core.h>
#include <wx/dcbuffer.h>
#include <wx/dcgraph.h>

#include <sstream>

#include "ap_state.h"
#include "game_data.h"
#include "global.h"
#include "tracker_state.h"

constexpr int AREA_ACTUAL_SIZE = 21;
constexpr int OWL_ACTUAL_SIZE = 32;

enum class ItemDrawType { kNone, kBox, kOwl };

SubwayMap::SubwayMap(wxWindow *parent) : wxPanel(parent, wxID_ANY) {
  SetBackgroundStyle(wxBG_STYLE_PAINT);

  map_image_ =
      wxImage(GetAbsolutePath("assets/subway.png").c_str(), wxBITMAP_TYPE_PNG);
  if (!map_image_.IsOk()) {
    return;
  }

  owl_image_ =
      wxImage(GetAbsolutePath("assets/owl.png").c_str(), wxBITMAP_TYPE_PNG);
  if (!owl_image_.IsOk()) {
    return;
  }

  unchecked_eye_ =
      wxBitmap(wxImage(GetAbsolutePath("assets/unchecked.png").c_str(),
                       wxBITMAP_TYPE_PNG)
                   .Scale(32, 32));
  checked_eye_ = wxBitmap(
      wxImage(GetAbsolutePath("assets/checked.png").c_str(), wxBITMAP_TYPE_PNG)
          .Scale(32, 32));

  tree_ = std::make_unique<quadtree::Quadtree<int, GetItemBox>>(
      quadtree::Box<float>{0, 0, static_cast<float>(map_image_.GetWidth()),
                           static_cast<float>(map_image_.GetHeight())});
  for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
    tree_->add(subway_item.id);
  }

  Redraw();

  scroll_timer_ = new wxTimer(this);

  Bind(wxEVT_PAINT, &SubwayMap::OnPaint, this);
  Bind(wxEVT_MOTION, &SubwayMap::OnMouseMove, this);
  Bind(wxEVT_MOUSEWHEEL, &SubwayMap::OnMouseScroll, this);
  Bind(wxEVT_LEAVE_WINDOW, &SubwayMap::OnMouseLeave, this);
  Bind(wxEVT_LEFT_DOWN, &SubwayMap::OnMouseClick, this);
  Bind(wxEVT_TIMER, &SubwayMap::OnTimer, this);

  zoom_slider_ = new wxSlider(this, wxID_ANY, 0, 0, 8, {15, 15});
  zoom_slider_->Bind(wxEVT_SLIDER, &SubwayMap::OnZoomSlide, this);

  help_button_ = new wxButton(this, wxID_ANY, "Help");
  help_button_->Bind(wxEVT_BUTTON, &SubwayMap::OnClickHelp, this);
  SetUpHelpButton();
}

void SubwayMap::OnConnect() {
  networks_.Clear();

  std::map<std::string, std::vector<int>> tagged;
  std::map<std::string, std::vector<int>> entrances;
  std::map<std::string, std::vector<int>> exits;
  for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
    if (AP_HasEarlyColorHallways() &&
        subway_item.special == "starting_room_paintings") {
      entrances["early_ch"].push_back(subway_item.id);
    }

    if (AP_IsPaintingShuffle() && !subway_item.paintings.empty()) {
      continue;
    }

    for (const std::string &tag : subway_item.tags) {
      tagged[tag].push_back(subway_item.id);
    }
    for (const std::string &tag : subway_item.entrances) {
      entrances[tag].push_back(subway_item.id);
    }
    for (const std::string &tag : subway_item.exits) {
      exits[tag].push_back(subway_item.id);
    }

    if (!AP_IsSunwarpShuffle() && subway_item.sunwarp) {
      std::string tag = fmt::format("sunwarp{}", subway_item.sunwarp->dots);
      switch (subway_item.sunwarp->type) {
        case SubwaySunwarpType::kEnter:
          entrances[tag].push_back(subway_item.id);
          break;
        case SubwaySunwarpType::kExit:
          exits[tag].push_back(subway_item.id);
          break;
        default:
          break;
      }
    }

    if (!AP_IsPilgrimageEnabled()) {
      if (subway_item.special == "sun_painting") {
        entrances["sun_painting"].push_back(subway_item.id);
      } else if (subway_item.special == "sun_painting_exit") {
        exits["sun_painting"].push_back(subway_item.id);
      }
    }
  }

  if (AP_IsSunwarpShuffle()) {
    sunwarp_mapping_ = AP_GetSunwarpMapping();

    SubwaySunwarp final_sunwarp{.dots = 6, .type = SubwaySunwarpType::kFinal};
    int final_sunwarp_item = GD_GetSubwayItemForSunwarp(final_sunwarp);

    for (const auto &[index, mapping] : sunwarp_mapping_) {
      std::string tag = fmt::format("sunwarp{}", mapping.dots);

      SubwaySunwarp fromWarp;
      if (index < 6) {
        fromWarp.dots = index + 1;
        fromWarp.type = SubwaySunwarpType::kEnter;
      } else {
        fromWarp.dots = index - 5;
        fromWarp.type = SubwaySunwarpType::kExit;
      }

      SubwaySunwarp toWarp;
      if (mapping.exit_index < 6) {
        toWarp.dots = mapping.exit_index + 1;
        toWarp.type = SubwaySunwarpType::kEnter;
      } else {
        toWarp.dots = mapping.exit_index - 5;
        toWarp.type = SubwaySunwarpType::kExit;
      }

      entrances[tag].push_back(GD_GetSubwayItemForSunwarp(fromWarp));
      exits[tag].push_back(GD_GetSubwayItemForSunwarp(toWarp));

      networks_.AddLinkToNetwork(
          final_sunwarp_item, GD_GetSubwayItemForSunwarp(fromWarp),
          mapping.dots == 6 ? final_sunwarp_item
                            : GD_GetSubwayItemForSunwarp(toWarp),
          false);
    }
  }

  for (const auto &[tag, items] : tagged) {
    // Pairwise connect all items with the same tag.
    for (auto tag_it1 = items.begin(); std::next(tag_it1) != items.end();
         tag_it1++) {
      for (auto tag_it2 = std::next(tag_it1); tag_it2 != items.end();
           tag_it2++) {
        // two links because tags are bi-directional
        networks_.AddLink(*tag_it1, *tag_it2, true);
      }
    }
  }

  for (const auto &[tag, items] : entrances) {
    if (!exits.contains(tag)) continue;
    for (auto exit : exits[tag]) {
      for (auto entrance : items) {
        networks_.AddLink(entrance, exit, false);
      }
    }
  }

  checked_paintings_.clear();
}

void SubwayMap::UpdateIndicators() {
  if (AP_IsSunwarpShuffle()) {
    sunwarp_mapping_ = AP_GetSunwarpMapping();
  }

  if (AP_IsPaintingShuffle()) {
    std::map<std::string, std::string> painting_mapping =
        AP_GetPaintingMapping();
    std::set<std::string> remote_checked_paintings = AP_GetCheckedPaintings();

    for (const std::string &painting_id : remote_checked_paintings) {
      if (!checked_paintings_.count(painting_id)) {
        checked_paintings_.insert(painting_id);

        if (painting_mapping.count(painting_id)) {
          std::optional<int> from_id = GD_GetSubwayItemForPainting(painting_id);
          std::optional<int> to_id = GD_GetSubwayItemForPainting(painting_mapping.at(painting_id));

          if (from_id && to_id) {
            networks_.AddLink(*from_id, *to_id, false);
          }
        }
      }
    }
  }

  Redraw();
}

void SubwayMap::UpdateSunwarp(SubwaySunwarp from_sunwarp,
                              SubwaySunwarp to_sunwarp) {
  networks_.AddLink(GD_GetSubwayItemForSunwarp(from_sunwarp),
                    GD_GetSubwayItemForSunwarp(to_sunwarp), false);
}

void SubwayMap::Zoom(bool in) {
  wxPoint focus_point;

  if (mouse_position_) {
    focus_point = *mouse_position_;
  } else {
    focus_point = {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2};
  }

  if (in) {
    if (zoom_ < 3.0) {
      SetZoom(zoom_ + 0.25, focus_point);
    }
  } else {
    if (zoom_ > 1.0) {
      SetZoom(zoom_ - 0.25, focus_point);
    }
  }
}

void SubwayMap::OnPaint(wxPaintEvent &event) {
  if (GetSize() != rendered_.GetSize()) {
    wxSize panel_size = GetSize();
    wxSize image_size = map_image_.GetSize();

    render_x_ = 0;
    render_y_ = 0;
    render_width_ = panel_size.GetWidth();
    render_height_ = panel_size.GetHeight();

    if (image_size.GetWidth() * panel_size.GetHeight() >
        panel_size.GetWidth() * image_size.GetHeight()) {
      render_height_ = (panel_size.GetWidth() * image_size.GetHeight()) /
                       image_size.GetWidth();
      render_y_ = (panel_size.GetHeight() - render_height_) / 2;
    } else {
      render_width_ = (image_size.GetWidth() * panel_size.GetHeight()) /
                      image_size.GetHeight();
      render_x_ = (panel_size.GetWidth() - render_width_) / 2;
    }

    SetZoomPos({zoom_x_, zoom_y_});

    SetUpHelpButton();
  }

  wxBufferedPaintDC dc(this);
  dc.SetBackground(*wxWHITE_BRUSH);
  dc.Clear();

  {
    wxMemoryDC rendered_dc;
    rendered_dc.SelectObject(rendered_);

    int dst_x;
    int dst_y;
    int dst_w;
    int dst_h;
    int src_x;
    int src_y;
    int src_w;
    int src_h;

    int zoomed_width = render_width_ * zoom_;
    int zoomed_height = render_height_ * zoom_;

    if (zoomed_width <= GetSize().GetWidth()) {
      dst_x = (GetSize().GetWidth() - zoomed_width) / 2;
      dst_w = zoomed_width;
      src_x = 0;
      src_w = map_image_.GetWidth();
    } else {
      dst_x = 0;
      dst_w = GetSize().GetWidth();
      src_x = -zoom_x_ * map_image_.GetWidth() / render_width_ / zoom_;
      src_w =
          GetSize().GetWidth() * map_image_.GetWidth() / render_width_ / zoom_;
    }

    if (zoomed_height <= GetSize().GetHeight()) {
      dst_y = (GetSize().GetHeight() - zoomed_height) / 2;
      dst_h = zoomed_height;
      src_y = 0;
      src_h = map_image_.GetHeight();
    } else {
      dst_y = 0;
      dst_h = GetSize().GetHeight();
      src_y = -zoom_y_ * map_image_.GetWidth() / render_width_ / zoom_;
      src_h =
          GetSize().GetHeight() * map_image_.GetWidth() / render_width_ / zoom_;
    }

    wxGCDC gcdc(dc);
    gcdc.GetGraphicsContext()->SetInterpolationQuality(wxINTERPOLATION_GOOD);
    gcdc.StretchBlit(dst_x, dst_y, dst_w, dst_h, &rendered_dc, src_x, src_y,
                     src_w, src_h);
  }

  if (hovered_item_) {
    // Note that these requirements are duplicated on OnMouseClick so that it
    // knows when an item has a hover effect.
    const SubwayItem &subway_item = GD_GetSubwayItem(*hovered_item_);
    std::optional<int> subway_door = GetRealSubwayDoor(subway_item);

    if (subway_door && !GetDoorRequirements(*subway_door).empty()) {
      const std::map<std::string, bool> &report =
          GetDoorRequirements(*subway_door);

      int acc_height = 10;
      int col_width = 0;

      for (const auto &[text, obtained] : report) {
        wxSize item_extent = dc.GetTextExtent(text);
        int item_height = std::max(32, item_extent.GetHeight()) + 10;
        acc_height += item_height;

        if (item_extent.GetWidth() > col_width) {
          col_width = item_extent.GetWidth();
        }
      }

      int item_width = col_width + 10 + 32;
      int full_width = item_width + 20;

      wxPoint popup_pos =
          MapPosToRenderPos({subway_item.x + AREA_ACTUAL_SIZE / 2,
                             subway_item.y + AREA_ACTUAL_SIZE / 2});

      if (popup_pos.x + full_width > GetSize().GetWidth()) {
        popup_pos.x = GetSize().GetWidth() - full_width;
      }
      if (popup_pos.y + acc_height > GetSize().GetHeight()) {
        popup_pos.y = GetSize().GetHeight() - acc_height;
      }

      dc.SetPen(*wxTRANSPARENT_PEN);
      dc.SetBrush(*wxBLACK_BRUSH);
      dc.DrawRectangle(popup_pos, {full_width, acc_height});

      dc.SetFont(GetFont());

      int cur_height = 10;

      for (const auto &[text, obtained] : report) {
        wxBitmap *eye_ptr = obtained ? &checked_eye_ : &unchecked_eye_;

        dc.DrawBitmap(*eye_ptr, popup_pos + wxPoint{10, cur_height});

        dc.SetTextForeground(obtained ? *wxWHITE : *wxRED);
        wxSize item_extent = dc.GetTextExtent(text);
        dc.DrawText(
            text,
            popup_pos +
                wxPoint{10 + 32 + 10,
                        cur_height + (32 - dc.GetFontMetrics().height) / 2});

        cur_height += 10 + 32;
      }
    }

    if (networks_.IsItemInNetwork(*hovered_item_)) {
      dc.SetBrush(*wxTRANSPARENT_BRUSH);

      for (const auto node : networks_.GetNetworkGraph(*hovered_item_)) {
        const SubwayItem &item1 = GD_GetSubwayItem(node.entry);
        const SubwayItem &item2 = GD_GetSubwayItem(node.exit);

        wxPoint item1_pos = MapPosToRenderPos(
            {item1.x + AREA_ACTUAL_SIZE / 2, item1.y + AREA_ACTUAL_SIZE / 2});
        wxPoint item2_pos = MapPosToRenderPos(
            {item2.x + AREA_ACTUAL_SIZE / 2, item2.y + AREA_ACTUAL_SIZE / 2});

        int left = std::min(item1_pos.x, item2_pos.x);
        int top = std::min(item1_pos.y, item2_pos.y);
        int right = std::max(item1_pos.x, item2_pos.x);
        int bottom = std::max(item1_pos.y, item2_pos.y);

        int halfwidth = right - left;
        int halfheight = bottom - top;

        if (halfwidth < 4 || halfheight < 4) {
          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
          dc.DrawLine(item1_pos, item2_pos);
          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
          dc.DrawLine(item1_pos, item2_pos);
          if (!node.two_way) {
            dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
            dc.SetBrush(*wxCYAN_BRUSH);
            dc.DrawCircle(item2_pos, 4);
            dc.SetBrush(*wxTRANSPARENT_BRUSH);
          }
        } else {
          int ellipse_x;
          int ellipse_y;
          double start;
          double end;

          if (item1_pos.x > item2_pos.x) {
            ellipse_y = top;

            if (item1_pos.y > item2_pos.y) {
              ellipse_x = left - halfwidth;

              start = 0;
              end = 90;
            } else {
              ellipse_x = left;

              start = 90;
              end = 180;
            }
          } else {
            ellipse_y = top - halfheight;

            if (item1_pos.y > item2_pos.y) {
              ellipse_x = left - halfwidth;

              start = 270;
              end = 360;
            } else {
              ellipse_x = left;

              start = 180;
              end = 270;
            }
          }

          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 4));
          dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
                             halfheight * 2, start, end);
          dc.SetPen(*wxThePenList->FindOrCreatePen(*wxCYAN, 2));
          dc.DrawEllipticArc(ellipse_x, ellipse_y, halfwidth * 2,
                             halfheight * 2, start, end);
          if (!node.two_way) {
            dc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 2));
            dc.SetBrush(*wxCYAN_BRUSH);
            dc.DrawCircle(item2_pos, 4);
            dc.SetBrush(*wxTRANSPARENT_BRUSH);
          }
        }
      }
    }
  }

  event.Skip();
}

void SubwayMap::OnMouseMove(wxMouseEvent &event) {
  wxPoint mouse_pos = RenderPosToMapPos(event.GetPosition());

  std::vector<int> hovered = tree_->query(
      {static_cast<float>(mouse_pos.x), static_cast<float>(mouse_pos.y), 2, 2});
  if (!hovered.empty()) {
    actual_hover_ = hovered[0];
  } else {
    actual_hover_ = std::nullopt;
  }

  if (!sticky_hover_ && actual_hover_ != hovered_item_) {
    hovered_item_ = actual_hover_;

    Refresh();
  }

  if (scroll_mode_) {
    EvaluateScroll(event.GetPosition());
  }

  mouse_position_ = event.GetPosition();

  event.Skip();
}

void SubwayMap::OnMouseScroll(wxMouseEvent &event) {
  double new_zoom = zoom_;
  if (event.GetWheelRotation() > 0) {
    new_zoom = std::min(3.0, zoom_ + 0.25);
  } else {
    new_zoom = std::max(1.0, zoom_ - 0.25);
  }

  if (zoom_ != new_zoom) {
    SetZoom(new_zoom, event.GetPosition());
  }

  event.Skip();
}

void SubwayMap::OnMouseLeave(wxMouseEvent &event) {
  SetScrollSpeed(0, 0);
  mouse_position_ = std::nullopt;
}

void SubwayMap::OnMouseClick(wxMouseEvent &event) {
  bool finished = false;

  if (actual_hover_) {
    const SubwayItem &subway_item = GD_GetSubwayItem(*actual_hover_);
    std::optional<int> subway_door = GetRealSubwayDoor(subway_item);

    if ((subway_door && !GetDoorRequirements(*subway_door).empty()) ||
        networks_.IsItemInNetwork(*hovered_item_)) {
      if (actual_hover_ != hovered_item_) {
        hovered_item_ = actual_hover_;

        if (!hovered_item_) {
          sticky_hover_ = false;
        }

        Refresh();
      } else {
        sticky_hover_ = !sticky_hover_;
      }

      finished = true;
    }
  }

  if (!finished) {
    if (scroll_mode_) {
      scroll_mode_ = false;

      SetScrollSpeed(0, 0);

      SetCursor(wxCURSOR_ARROW);
    } else if (event.GetPosition().x < GetSize().GetWidth() / 6 ||
               event.GetPosition().x > 5 * GetSize().GetWidth() / 6 ||
               event.GetPosition().y < GetSize().GetHeight() / 6 ||
               event.GetPosition().y > 5 * GetSize().GetHeight() / 6) {
      scroll_mode_ = true;

      EvaluateScroll(event.GetPosition());

      SetCursor(wxCURSOR_CROSS);
    } else {
      sticky_hover_ = false;
    }
  }
}

void SubwayMap::OnTimer(wxTimerEvent &event) {
  SetZoomPos({zoom_x_ + scroll_x_, zoom_y_ + scroll_y_});
  Refresh();
}

void SubwayMap::OnZoomSlide(wxCommandEvent &event) {
  double new_zoom = 1.0 + 0.25 * zoom_slider_->GetValue();

  if (new_zoom != zoom_) {
    SetZoom(new_zoom, {GetSize().GetWidth() / 2, GetSize().GetHeight() / 2});
  }
}

void SubwayMap::OnClickHelp(wxCommandEvent &event) {
  wxMessageBox(
      "Zoom in/out using the mouse wheel, Ctrl +/-, or the slider in the "
      "corner.\nClick on a side of the screen to start panning. It will follow "
      "your mouse. Click again to stop.\nHover over a door to see the "
      "requirements to open it.\nHover over a warp or active painting to see "
      "what it is connected to.\nFor one-way connections, there will be a "
      "circle at the exit.\nIn painting shuffle, paintings that have not "
      "yet been checked will not show their connections.\nA green shaded owl "
      "means that there is a painting entrance there.\nA red shaded owl means "
      "that there are only painting exits there.\nClick on a door or "
      "warp to make the popup stick until you click again.",
      "Subway Map Help");
}

void SubwayMap::Redraw() {
  rendered_ = wxBitmap(map_image_);

  wxMemoryDC dc;
  dc.SelectObject(rendered_);

  wxGCDC gcdc(dc);

  std::map<std::string, std::string> painting_mapping = AP_GetPaintingMapping();

  for (const SubwayItem &subway_item : GD_GetSubwayItems()) {
    ItemDrawType draw_type = ItemDrawType::kNone;
    const wxBrush *brush_color = wxGREY_BRUSH;
    std::optional<wxColour> shade_color;
    std::optional<int> subway_door = GetRealSubwayDoor(subway_item);

    if (AP_HasEarlyColorHallways() &&
        subway_item.special == "starting_room_paintings") {
      draw_type = ItemDrawType::kOwl;
      shade_color = wxColour(0, 255, 0, 128);
    } else if (subway_item.special == "sun_painting") {
      if (!AP_IsPilgrimageEnabled()) {
        if (IsDoorOpen(*subway_item.door)) {
          draw_type = ItemDrawType::kOwl;
          shade_color = wxColour(0, 255, 0, 128);
        } else {
          draw_type = ItemDrawType::kBox;
          brush_color = wxRED_BRUSH;
        }
      }
    } else if (subway_item.sunwarp &&
               subway_item.sunwarp->type == SubwaySunwarpType::kFinal &&
               AP_IsPilgrimageEnabled()) {
      draw_type = ItemDrawType::kBox;

      if (IsPilgrimageDoable()) {
        brush_color = wxGREEN_BRUSH;
      } else {
        brush_color = wxRED_BRUSH;
      }
    } else if (!subway_item.paintings.empty()) {
      if (AP_IsPaintingShuffle()) {
        bool has_checked_painting = false;
        bool has_unchecked_painting = false;
        bool has_mapped_painting = false;
        bool has_codomain_painting = false;

        for (const std::string &painting_id : subway_item.paintings) {
          if (checked_paintings_.count(painting_id)) {
            has_checked_painting = true;

            if (painting_mapping.count(painting_id)) {
              has_mapped_painting = true;
            } else if (AP_IsPaintingMappedTo(painting_id)) {
              has_codomain_painting = true;
            }
          } else {
            has_unchecked_painting = true;
          }
        }

        if (has_unchecked_painting || has_mapped_painting ||
            has_codomain_painting) {
          draw_type = ItemDrawType::kOwl;

          if (has_checked_painting) {
            if (has_mapped_painting) {
              shade_color = wxColour(0, 255, 0, 128);
            } else {
              shade_color = wxColour(255, 0, 0, 128);
            }
          }
        }
      } else if (subway_item.HasWarps()) {
        draw_type = ItemDrawType::kOwl;
      }
    } else if (subway_door) {
      draw_type = ItemDrawType::kBox;

      if (IsDoorOpen(*subway_door)) {
        brush_color = wxGREEN_BRUSH;
      } else {
        brush_color = wxRED_BRUSH;
      }
    }

    wxPoint real_area_pos = {subway_item.x, subway_item.y};

    int real_area_size =
        (draw_type == ItemDrawType::kOwl ? OWL_ACTUAL_SIZE : AREA_ACTUAL_SIZE);

    if (draw_type == ItemDrawType::kBox) {
      gcdc.SetPen(*wxThePenList->FindOrCreatePen(*wxBLACK, 1));
      gcdc.SetBrush(*brush_color);
      gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
    } else if (draw_type == ItemDrawType::kOwl) {
      wxBitmap owl_bitmap = wxBitmap(owl_image_.Scale(
          real_area_size, real_area_size, wxIMAGE_QUALITY_BILINEAR));
      gcdc.DrawBitmap(owl_bitmap, real_area_pos);

      if (shade_color) {
        gcdc.SetBrush(wxBrush(*shade_color));
        gcdc.DrawRectangle(real_area_pos, {real_area_size, real_area_size});
      }
    }
  }
}

void SubwayMap::SetUpHelpButton() {
  help_button_->SetPosition({
      GetSize().GetWidth() - help_button_->GetSize().GetWidth() - 15,
      15,
  });
}

void SubwayMap::EvaluateScroll(wxPoint pos) {
  int scroll_x;
  int scroll_y;
  if (pos.x < GetSize().GetWidth() / 9) {
    scroll_x = 20;
  } else if (pos.x < GetSize().GetWidth() / 6) {
    scroll_x = 5;
  } else if (pos.x > 8 * GetSize().GetWidth() / 9) {
    scroll_x = -20;
  } else if (pos.x > 5 * GetSize().GetWidth() / 6) {
    scroll_x = -5;
  } else {
    scroll_x = 0;
  }
  if (pos.y < GetSize().GetHeight() / 9) {
    scroll_y = 20;
  } else if (pos.y < GetSize().GetHeight() / 6) {
    scroll_y = 5;
  } else if (pos.y > 8 * GetSize().GetHeight() / 9) {
    scroll_y = -20;
  } else if (pos.y > 5 * GetSize().GetHeight() / 6) {
    scroll_y = -5;
  } else {
    scroll_y = 0;
  }

  SetScrollSpeed(scroll_x, scroll_y);
}

wxPoint SubwayMap::MapPosToRenderPos(wxPoint pos) const {
  return {static_cast<int>(pos.x * render_width_ * zoom_ /
                               map_image_.GetSize().GetWidth() +
                           zoom_x_),
          static_cast<int>(pos.y * render_width_ * zoom_ /
                               map_image_.GetSize().GetWidth() +
                           zoom_y_)};
}

wxPoint SubwayMap::MapPosToVirtualPos(wxPoint pos) const {
  return {static_cast<int>(pos.x * render_width_ * zoom_ /
                           map_image_.GetSize().GetWidth()),
          static_cast<int>(pos.y * render_width_ * zoom_ /
                           map_image_.GetSize().GetWidth())};
}

wxPoint SubwayMap::RenderPosToMapPos(wxPoint pos) const {
  return {
      std::clamp(static_cast<int>((pos.x - zoom_x_) * map_image_.GetWidth() /
                                  render_width_ / zoom_),
                 0, map_image_.GetWidth() - 1),
      std::clamp(static_cast<int>((pos.y - zoom_y_) * map_image_.GetWidth() /
                                  render_width_ / zoom_),
                 0, map_image_.GetHeight() - 1)};
}

void SubwayMap::SetZoomPos(wxPoint pos) {
  if (render_width_ * zoom_ <= GetSize().GetWidth()) {
    zoom_x_ = (GetSize().GetWidth() - render_width_ * zoom_) / 2;
  } else {
    zoom_x_ = std::clamp(
        pos.x, GetSize().GetWidth() - static_cast<int>(render_width_ * zoom_),
        0);
  }
  if (render_height_ * zoom_ <= GetSize().GetHeight()) {
    zoom_y_ = (GetSize().GetHeight() - render_height_ * zoom_) / 2;
  } else {
    zoom_y_ = std::clamp(
        pos.y, GetSize().GetHeight() - static_cast<int>(render_height_ * zoom_),
        0);
  }
}

void SubwayMap::SetScrollSpeed(int scroll_x, int scroll_y) {
  bool should_timer = (scroll_x != 0 || scroll_y != 0);
  if (should_timer != scroll_timer_->IsRunning()) {
    if (should_timer) {
      scroll_timer_->Start(1000 / 60);
    } else {
      scroll_timer_->Stop();
    }
  }

  scroll_x_ = scroll_x;
  scroll_y_ = scroll_y;
}

void SubwayMap::SetZoom(double zoom, wxPoint static_point) {
  wxPoint map_pos = RenderPosToMapPos(static_point);
  zoom_ = zoom;

  wxPoint virtual_pos = MapPosToVirtualPos(map_pos);
  SetZoomPos(-(virtual_pos - static_point));

  Refresh();

  zoom_slider_->SetValue((zoom - 1.0) / 0.25);
}

std::optional<int> SubwayMap::GetRealSubwayDoor(const SubwayItem subway_item) {
  if (AP_IsSunwarpShuffle() && subway_item.sunwarp &&
      subway_item.sunwarp->type != SubwaySunwarpType::kFinal) {
    int sunwarp_index = subway_item.sunwarp->dots - 1;
    if (subway_item.sunwarp->type == SubwaySunwarpType::kExit) {
      sunwarp_index += 6;
    }

    for (const auto &[start_index, mapping] : sunwarp_mapping_) {
      if (start_index == sunwarp_index || mapping.exit_index == sunwarp_index) {
        return GD_GetSunwarpDoors().at(mapping.dots - 1);
      }
    }
  }

  return subway_item.door;
}

quadtree::Box<float> SubwayMap::GetItemBox::operator()(const int &id) const {
  const SubwayItem &subway_item = GD_GetSubwayItem(id);
  return {static_cast<float>(subway_item.x), static_cast<float>(subway_item.y),
          AREA_ACTUAL_SIZE, AREA_ACTUAL_SIZE};
}