#include "StarMainInterface.hpp"
#include "StarJsonExtra.hpp"
#include "StarLogging.hpp"
#include "StarLexicalCast.hpp"
#include "StarContainerInterface.hpp"
#include "StarCraftingInterface.hpp"
#include "StarMerchantInterface.hpp"
#include "StarRoot.hpp"
#include "StarUniverseClient.hpp"
#include "StarCodexInterface.hpp"
#include "StarSongbookInterface.hpp"
#include "StarQuestInterface.hpp"
#include "StarQuestManager.hpp"
#include "StarPopupInterface.hpp"
#include "StarConfirmationDialog.hpp"
#include "StarJoinRequestDialog.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarGuiReader.hpp"
#include "StarPaneManager.hpp"
#include "StarClientCommandProcessor.hpp"
#include "StarChat.hpp"
#include "StarOptionsMenu.hpp"
#include "StarActionBar.hpp"
#include "StarWireInterface.hpp"
#include "StarTeamBar.hpp"
#include "StarStatusPane.hpp"
#include "StarCanvasWidget.hpp"
#include "StarLabelWidget.hpp"
#include "StarItemSlotWidget.hpp"
#include "StarPlayer.hpp"
#include "StarPlayerLog.hpp"
#include "StarMonster.hpp"
#include "StarItemDrop.hpp"
#include "StarAssets.hpp"
#include "StarPlayerInventory.hpp"
#include "StarCelestialDatabase.hpp"
#include "StarItem.hpp"
#include "StarAiInterface.hpp"
#include "StarDrawable.hpp"
#include "StarFireableItem.hpp"
#include "StarClientContext.hpp"
#include "StarToolUserEntity.hpp"
#include "StarTeleportDialog.hpp"
#include "StarCinematic.hpp"
#include "StarNameplatePainter.hpp"
#include "StarQuestIndicatorPainter.hpp"
#include "StarScriptPane.hpp"
#include "StarContainerEntity.hpp"
#include "StarWarpTargetEntity.hpp"
#include "StarPlayerUniverseMap.hpp"
#include "StarWorldTemplate.hpp"
#include "StarRadioMessagePopup.hpp"
#include "StarAiTypes.hpp"
#include "StarActiveItem.hpp"
#include "StarInspectionTool.hpp"
#include "StarQuestTracker.hpp"
#include "StarContainerInteractor.hpp"
#include "StarChatBubbleManager.hpp"
#include "StarNpc.hpp"

namespace Star {

GuiMessage::GuiMessage() : message(), cooldown(), springState() {}

GuiMessage::GuiMessage(String const& message, float cooldown, float spring)
  : message(message), cooldown(cooldown), springState(spring) {}

MainInterface::MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay)
  : m_guiContext(GuiContext::singletonPtr())
  , m_config(MainInterfaceConfig::loadFromAssets())
  , m_client(std::move(client))
  , m_worldPainter(std::move(painter))
  , m_cinematicOverlay(std::move(cinematicOverlay))
  , m_containerInteractor(make_shared<ContainerInteractor>())
{
  GuiReader itemSlotReader;
  m_cursorItem = convert<ItemSlotWidget>(itemSlotReader.makeSingle("cursorItemSlot", m_config->cursorItemSlot));

  m_planetNameTimer = GameTimer(m_config->planetNameTime);

  m_debugSpatialClearTimer = GameTimer(m_config->debugSpatialClearTime);
  m_debugMapClearTimer = GameTimer(m_config->debugMapClearTime);

  m_stickyTargetingTimer = GameTimer(m_config->monsterHealthBarTime);

  m_inventoryWindow = make_shared<InventoryPane>(this, m_client->mainPlayer(), m_containerInteractor);
  m_paneManager.registerPane(MainInterfacePanes::Inventory, PaneLayer::Window, m_inventoryWindow, [this](PanePtr const&) {
      if (auto player = m_client->mainPlayer())
          player->clearSwap();
      if (m_containerPane) {
        m_containerPane->dismiss();
        m_containerPane = {};
        m_containerInteractor->closeContainer();
      }
      for (EntityId id : m_interactionScriptPanes.keys()) {
        if (m_paneManager.isDisplayed(m_interactionScriptPanes[id]) && as<ScriptPane>(m_interactionScriptPanes[id])->openWithInventory())
          m_interactionScriptPanes[id]->dismiss();
      }
    });

  m_overflowMessage = make_shared<GuiMessage>("", 0);

  m_plainCraftingWindow = make_shared<CraftingPane>(m_client->worldClient(), m_client->mainPlayer(), JsonObject{{"filter", JsonArray{"plain"}}}, m_client->mainPlayer()->entityId());
  m_paneManager.registerPane(MainInterfacePanes::CraftingPlain, PaneLayer::Window, m_plainCraftingWindow);

  m_paneManager.registerPane(MainInterfacePanes::EscapeDialog, PaneLayer::ModalWindow, createEscapeDialog());

  auto songbookInterface = make_shared<SongbookInterface>(m_client->mainPlayer());
  m_paneManager.registerPane(MainInterfacePanes::Songbook, PaneLayer::Window, songbookInterface);

  m_questLogInterface = make_shared<QuestLogInterface>(m_client->questManager(), m_client->mainPlayer(), m_cinematicOverlay, m_client);
  m_paneManager.registerPane(MainInterfacePanes::QuestLog, PaneLayer::Window, m_questLogInterface);

  auto aiInterface = make_shared<AiInterface>(m_client, m_cinematicOverlay, &m_paneManager);
  m_paneManager.registerPane(MainInterfacePanes::Ai, PaneLayer::Window, aiInterface);

  m_codexInterface = make_shared<CodexInterface>(m_client->mainPlayer());
  m_paneManager.registerPane(MainInterfacePanes::Codex, PaneLayer::Window, m_codexInterface);

  m_optionsMenu = make_shared<OptionsMenu>(&m_paneManager);
  m_paneManager.registerPane(MainInterfacePanes::Options, PaneLayer::ModalWindow, m_optionsMenu);

  m_popupInterface = make_shared<PopupInterface>();
  m_paneManager.registerPane(MainInterfacePanes::Popup, PaneLayer::Window, m_popupInterface);

  m_confirmationDialog = make_shared<ConfirmationDialog>();
  m_paneManager.registerPane(MainInterfacePanes::Confirmation, PaneLayer::ModalWindow, m_confirmationDialog);

  m_joinRequestDialog = make_shared<JoinRequestDialog>();
  m_paneManager.registerPane(MainInterfacePanes::JoinRequest, PaneLayer::ModalWindow, m_joinRequestDialog);

  m_actionBar = make_shared<ActionBar>(&m_paneManager, m_client->mainPlayer());
  m_paneManager.registerPane(MainInterfacePanes::ActionBar, PaneLayer::Hud, m_actionBar);

  m_questTracker = make_shared<QuestTrackerPane>();
  m_paneManager.registerPane(MainInterfacePanes::QuestTracker, PaneLayer::Hud, m_questTracker);

  m_mmUpgrade = make_shared<ScriptPane>(m_client, "/interface/scripted/mmupgrade/mmupgradegui.config");
  m_paneManager.registerPane(MainInterfacePanes::MmUpgrade, PaneLayer::Window, m_mmUpgrade);

  m_collections = make_shared<ScriptPane>(m_client, "/interface/scripted/collections/collectionsgui.config");
  m_paneManager.registerPane(MainInterfacePanes::Collections, PaneLayer::Window, m_collections);

  m_chat = make_shared<Chat>(m_client);
  m_paneManager.registerPane(MainInterfacePanes::Chat, PaneLayer::Hud, m_chat);
  m_clientCommandProcessor = make_shared<ClientCommandProcessor>(m_client, m_cinematicOverlay, &m_paneManager, m_config->macroCommands);

  m_radioMessagePopup = make_shared<RadioMessagePopup>();
  m_paneManager.registerPane(MainInterfacePanes::RadioMessagePopup, PaneLayer::Hud, m_radioMessagePopup);

  m_wireInterface = make_shared<WirePane>(m_client->worldClient(), m_client->mainPlayer(), m_worldPainter);
  m_paneManager.registerPane(MainInterfacePanes::WireInterface, PaneLayer::World, m_wireInterface);
  m_client->mainPlayer()->setWireConnector(m_wireInterface.get());

  auto teamBar = make_shared<TeamBar>(this, m_client);
  m_paneManager.registerPane(MainInterfacePanes::TeamBar, PaneLayer::Hud, teamBar);

  auto statusPane = make_shared<StatusPane>(&m_paneManager, m_client);
  m_paneManager.registerPane(MainInterfacePanes::StatusPane, PaneLayer::Hud, statusPane);

  auto planetName = make_shared<Pane>();
  m_planetText = make_shared<LabelWidget>();
  m_planetText->setFontSize(m_config->planetNameFontSize);
  m_planetText->setFontMode(FontMode::Normal);
  m_planetText->setAnchor(HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor);
  m_planetText->setDirectives(m_config->planetNameDirectives);
  planetName->disableScissoring();
  planetName->setPosition(m_config->planetNameOffset);
  planetName->setAnchor(PaneAnchor::Center);
  planetName->addChild("planetText", m_planetText);
  m_paneManager.registerPane(MainInterfacePanes::PlanetText, PaneLayer::Hud, planetName);

  m_nameplatePainter = make_shared<NameplatePainter>();
  m_questIndicatorPainter = make_shared<QuestIndicatorPainter>(m_client);
  m_chatBubbleManager = make_shared<ChatBubbleManager>();

  m_paneManager.displayRegisteredPane(MainInterfacePanes::ActionBar);
  m_paneManager.displayRegisteredPane(MainInterfacePanes::Chat);
  m_paneManager.displayRegisteredPane(MainInterfacePanes::TeamBar);
  m_paneManager.displayRegisteredPane(MainInterfacePanes::StatusPane);
}

MainInterface::~MainInterface() {
  m_paneManager.dismissAllPanes();
}

MainInterface::RunningState MainInterface::currentState() const {
  return m_state;
}

MainInterfacePaneManager* MainInterface::paneManager() {
  return &m_paneManager;
}

bool MainInterface::escapeDialogOpen() const {
  return m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::EscapeDialog) || m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Options);
}

void MainInterface::openCraftingWindow(Json const& config, EntityId sourceEntityId) {
  if (m_craftingWindow && m_paneManager.isDisplayed(m_craftingWindow)) {
    m_paneManager.dismissPane(m_craftingWindow);
    if (sourceEntityId != NullEntityId && m_craftingWindow->sourceEntityId() == sourceEntityId) {
      m_craftingWindow.reset();
      return;
    }
  }

  m_craftingWindow = make_shared<CraftingPane>(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId);
  m_paneManager.displayPane(PaneLayer::Window, m_craftingWindow, [this](PanePtr const&) {
    if (auto player = m_client->mainPlayer())
      player->clearSwap();
    });
}

void MainInterface::openMerchantWindow(Json const& config, EntityId sourceEntityId) {
  if (m_merchantWindow && m_paneManager.isDisplayed(m_merchantWindow)) {
    m_paneManager.dismissPane(m_merchantWindow);
    if (sourceEntityId != NullEntityId && m_merchantWindow->sourceEntityId() == sourceEntityId) {
      m_merchantWindow.reset();
      return;
    }
  }

  bool openWithInventory = config.getBool("openWithInventory", true);
  m_merchantWindow = make_shared<MerchantPane>(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId);
  m_paneManager.displayPane(PaneLayer::Window,
      m_merchantWindow,
      [this, openWithInventory](PanePtr const&) {
      if (auto player = m_client->mainPlayer())
        player->clearSwap();
      if (openWithInventory)
        m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
    });
  if (openWithInventory)
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);

  m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
    m_merchantWindow, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
}

void MainInterface::togglePlainCraftingWindow() {
  m_paneManager.toggleRegisteredPane(MainInterfacePanes::CraftingPlain);

  if (m_craftingWindow && m_craftingWindow->isDisplayed()
      && m_craftingWindow != m_paneManager.registeredPane(MainInterfacePanes::CraftingPlain))
    m_paneManager.dismissPane(m_craftingWindow);

  m_craftingWindow = m_plainCraftingWindow;
}

bool MainInterface::windowsOpen() const {
  return (bool)m_paneManager.topPane({PaneLayer::Window});
}

MerchantPanePtr MainInterface::activeMerchantPane() const {
  if (m_paneManager.isDisplayed(m_merchantWindow))
    return m_merchantWindow;
  else
    return {};
}

bool MainInterface::handleInputEvent(InputEvent const& event) {
  auto player = m_client->mainPlayer();
  auto inv = player->inventory();
  auto& root = Root::singleton();

  if (auto mouseMove = event.ptr<MouseMoveEvent>())
    m_cursorScreenPos = mouseMove->mousePosition;

  if (m_paneManager.sendInputEvent(event)) {
    if (!event.is<MouseButtonUpEvent>() && !event.is<KeyUpEvent>())
      return true;
  }

  if (event.is<KeyDownEvent>()) {
    if (m_chat->hasFocus()) {
      if (m_guiContext->actions(event).contains(InterfaceAction::ChatSendLine)) {
        doChat(m_chat->currentChat(), true);
        m_chat->clearCurrentChat();
        m_chat->stopChat();
        return true;
      }
    } else if (!m_paneManager.keyboardCapturedPane()) {
      Maybe<InventorySlot> swapSlot;

      for (auto action : m_guiContext->actions(event)) {
        switch (action) {
          default:
            break;
          case InterfaceAction::GuiShifting:
            m_guiContext->setShiftHeld(true);
            break;
          case InterfaceAction::ChatBegin:
            m_chat->startChat();
            break;

          case InterfaceAction::InterfaceHideHud:
            m_disableHud = !m_disableHud;
            break;

          case InterfaceAction::InterfaceRepeatCommand:
            if (!m_lastCommand.empty())
              doChat(m_lastCommand, false);
            break;

          case InterfaceAction::InterfaceToggleFullscreen:
            m_optionsMenu->toggleFullscreen();
            break;

          case InterfaceAction::InterfaceReload:
            root.reload();
            root.fullyLoad();
            break;

          case InterfaceAction::ChatBeginCommand:
            m_chat->startCommand();
            break;

          case InterfaceAction::InterfaceEscapeMenu:
            m_paneManager.toggleRegisteredPane(MainInterfacePanes::EscapeDialog);
            break;

          case InterfaceAction::InterfaceInventory:
            m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory);
            break;

          case InterfaceAction::InterfaceCodex:
            m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
            break;

          case InterfaceAction::InterfaceQuest:
            m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
            break;

          case InterfaceAction::InterfaceCrafting:
            togglePlainCraftingWindow();
            break;
        }
      }
    }

    return false;

  } else if (auto keyUp = event.ptr<KeyUpEvent>()) {
    if (m_guiContext->actionsForKey(keyUp->key).contains(InterfaceAction::GuiShifting))
      m_guiContext->setShiftHeld(false);

    return false;

  } else if (auto mouseDown = event.ptr<MouseButtonDownEvent>()) {
    auto mouseButton = mouseDown->mouseButton;
    if (mouseButton >= MouseButton::Left && mouseButton <= MouseButton::Right
    && overlayClick(mouseDown->mousePosition, mouseDown->mouseButton)) {
      return true;
    } else {
      if (mouseButton == MouseButton::Left)
        player->beginPrimaryFire();
      if (mouseButton == MouseButton::Right)
        player->beginAltFire();
      if (mouseButton == MouseButton::Middle)
        player->beginTrigger();
    }
  } else if (auto mouseUp = event.ptr<MouseButtonUpEvent>()) {
    if (mouseUp->mouseButton == MouseButton::Left)
      player->endPrimaryFire();
    if (mouseUp->mouseButton == MouseButton::Right)
      player->endAltFire();
    if (mouseUp->mouseButton == MouseButton::Middle)
      player->endTrigger();
  }

  bool captured = false;

  for (auto& pair : m_canvases)
    captured |= pair.second->sendEvent(event);

  return captured;
}

bool MainInterface::inputFocus() const {
  return (bool)m_paneManager.keyboardCapturedPane();
}

bool MainInterface::textInputActive() const {
  return m_paneManager.keyboardCapturedForTextInput();
}

void MainInterface::handleInteractAction(InteractAction interactAction) {
  auto assets = Root::singleton().assets();
  auto world = m_client->worldClient();

  if (interactAction.type == InteractActionType::OpenContainer) {
    // If we're currently displaying this container, close it.
    if (m_containerPane && m_containerInteractor->openContainerId() == interactAction.entityId) {
      m_paneManager.dismissPane(m_containerPane);
      return;
    }

    // If we're currently displaying another container, close it before we open.
    if (m_containerPane)
      m_paneManager.dismissPane(m_containerPane);

    auto containerEntity = world->get<ContainerEntity>(interactAction.entityId);
    if (!containerEntity)
      return;

    m_containerInteractor->openContainer(containerEntity);

    m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);

    m_containerPane = make_shared<ContainerPane>(world, m_client->mainPlayer(), m_containerInteractor);
    m_paneManager.displayPane(PaneLayer::Window, m_containerPane, [this](PanePtr const&) {
      if (auto player = m_client->mainPlayer())
        player->clearSwap();
      m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
    });

    m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
        m_containerPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
  } else if (interactAction.type == InteractActionType::SitDown) {
    m_client->mainPlayer()->lounge(interactAction.entityId, interactAction.data.toUInt());
  } else if (interactAction.type == InteractActionType::OpenCraftingInterface) {
    if (interactAction.entityId != NullEntityId && !world->entity(interactAction.entityId))
      return;

    openCraftingWindow(interactAction.data, interactAction.entityId);
  } else if (interactAction.type == InteractActionType::OpenSongbookInterface) {
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Songbook);
  } else if (interactAction.type == InteractActionType::OpenNpcCraftingInterface) {
    if (interactAction.entityId != NullEntityId && !world->entity(interactAction.entityId))
      return;
    // wait, this is literally the exact same as OpenCraftingInterface. what the fuck? lol
    openCraftingWindow(interactAction.data, interactAction.entityId);
  } else if (interactAction.type == InteractActionType::OpenMerchantInterface) {
    if (interactAction.entityId != NullEntityId && !world->entity(interactAction.entityId))
      return;

    openMerchantWindow(interactAction.data, interactAction.entityId);
  } else if (interactAction.type == InteractActionType::OpenAiInterface) {
    as<AiInterface>(m_paneManager.registeredPane(MainInterfacePanes::Ai))->setSourceEntityId(interactAction.entityId);
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Ai);
  } else if (interactAction.type == InteractActionType::OpenTeleportDialog) {
    if (m_teleportDialog)
      m_teleportDialog->dismiss();

    if (!m_client->canTeleport())
      return;

    auto currentLocation = TeleportBookmark();

    auto config = assets->fetchJson(interactAction.data);
    if (config.getBool("canBookmark", false)) {
      if (auto entity = world->entity(interactAction.entityId)) {
        if (auto uniqueEntityId = entity->uniqueId()) {
          auto worldTemplate = m_client->worldClient()->currentTemplate();

          String icon, planetName;
          if (m_client->playerWorld().is<ClientShipWorldId>()) {
            icon = "ship";
            planetName = "Player Ship";
          } else if (m_client->playerWorld().is<CelestialWorldId>()) {
            icon = worldTemplate->worldParameters()->typeName;
            planetName = worldTemplate->worldName();
          } else if (m_client->playerWorld().is<InstanceWorldId>()) {
            icon = worldTemplate->worldParameters()->typeName;
            planetName = worldTemplate->worldName();
          } else {
            icon = "default";
            planetName = "???";
          }

          currentLocation = TeleportBookmark {
            {m_client->playerWorld(), SpawnTargetUniqueEntity(*uniqueEntityId)},
            planetName,
            config.getString("bookmarkName", ""),
            icon
          };

          if (!m_client->mainPlayer()->universeMap()->teleportBookmarks().contains(currentLocation) || !config.getBool("canTeleport", true)) {
            auto editBookmarkDialog = make_shared<EditBookmarkDialog>(m_client->mainPlayer()->universeMap());
            editBookmarkDialog->setBookmark(currentLocation);
            m_paneManager.displayPane(PaneLayer::ModalWindow, editBookmarkDialog);
            return;
          }
        }
      }
    }

    if (config.getBool("canTeleport", true)) {
      m_teleportDialog = make_shared<TeleportDialog>(m_client, &m_paneManager, interactAction.data, interactAction.entityId, currentLocation);
      m_paneManager.displayPane(PaneLayer::ModalWindow, m_teleportDialog);
    }
  } else if (interactAction.type == InteractActionType::ShowPopup) {
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Popup);
    m_popupInterface->displayMessage(interactAction.data.getString("message"), interactAction.data.getString("title", ""), interactAction.data.getString("subtitle", ""), interactAction.data.optString("sound"));
  } else if (interactAction.type == InteractActionType::ScriptPane) {
    auto sourceEntity = interactAction.entityId;
    // dismiss if there's already a scriptpane open for this source entity
    if (sourceEntity != NullEntityId && m_interactionScriptPanes.contains(sourceEntity) && m_paneManager.isDisplayed(m_interactionScriptPanes[sourceEntity]))
      m_paneManager.dismissPane(m_interactionScriptPanes[sourceEntity]);

    ScriptPanePtr scriptPane = make_shared<ScriptPane>(m_client, interactAction.data, sourceEntity);
    displayScriptPane(scriptPane, sourceEntity);

  } else if (interactAction.type == InteractActionType::Message) {
    m_client->mainPlayer()->receiveMessage(connectionForEntity(interactAction.entityId),
        interactAction.data.getString("messageType"), interactAction.data.getArray("messageArgs"));
  }
}

void MainInterface::preUpdate(float dt) {
  auto player = m_client->mainPlayer();
  if (!m_client->paused())
    player->aim(cursorWorldPosition());
}

void MainInterface::update(float dt) {
  m_paneManager.update(dt);
  m_cursor.update(dt);

  m_questLogInterface->pollDialog(&m_paneManager);

  if (!m_paneManager.topPane({PaneLayer::ModalWindow}) && m_codexInterface->showNewCodex())
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Codex);

  auto player = m_client->mainPlayer();
  auto cursorWorldPos = cursorWorldPosition();
  if (player->wireToolInUse()) {
    m_paneManager.displayRegisteredPane(MainInterfacePanes::WireInterface);
    player->setWireConnector(m_wireInterface.get());
  } else {
    m_paneManager.dismissRegisteredPane(MainInterfacePanes::WireInterface);
  }

  // update inventory pane items, to know if item slots changed
  m_inventoryWindow->updateItems();

  // update mouseover target
  EntityId newMouseOverTarget = NullEntityId;
  m_stickyTargetingTimer.tick(dt);
  auto mouseoverEntities = m_client->worldClient()->query<DamageBarEntity>(RectF::withCenter(cursorWorldPos, Vec2F(1, 1)), [=](shared_ptr<DamageBarEntity> const& entity) {
      return entity != player
        && entity->damageBar() == DamageBarType::Default
        && (entity->getTeam().type == TeamType::Enemy || entity->getTeam().type == TeamType::PVP)
        && m_client->worldClient()->lightLevel(entity->position()) > 0;
    });
  sortByComputedValue(mouseoverEntities, [&](DamageBarEntityPtr const& a) {
      return m_client->worldClient()->geometry().diff(a->position(), cursorWorldPos).magnitude();
    });
  if (mouseoverEntities.size() > 0) {
    newMouseOverTarget = mouseoverEntities[0]->entityId();
  } else if (m_lastMouseoverTarget == NullEntityId && player->lastDamagedTarget() != NullEntityId && player->timeSinceLastGaveDamage() < m_stickyTargetingTimer.time / 2) {
    if (auto targetEntity = as<DamageBarEntity>(m_client->worldClient()->entity(player->lastDamagedTarget()))) {
      if (targetEntity->damageBar() == DamageBarType::Default && (targetEntity->getTeam().type == TeamType::Enemy || targetEntity->getTeam().type == TeamType::PVP)) {
        newMouseOverTarget = targetEntity->entityId();
      }
    }
  }
  if (newMouseOverTarget != NullEntityId && newMouseOverTarget != m_lastMouseoverTarget) {
    m_lastMouseoverTarget = newMouseOverTarget;
    m_portraitScale = 0;
    m_stickyTargetingTimer.reset();
  }

  if (m_stickyTargetingTimer.ready())
    m_lastMouseoverTarget = NullEntityId;

  // special damage bar entity
  if (m_specialDamageBarTarget != NullEntityId) {
    auto damageBarEntity = as<DamageBarEntity>(m_client->worldClient()->entity(m_specialDamageBarTarget));
    if (damageBarEntity && damageBarEntity->damageBar() == DamageBarType::Special) {
      float targetHealth = damageBarEntity->health() / damageBarEntity->maxHealth();
      float fillSpeed = 1.0f / Root::singleton().assets()->json("/interface.config:specialDamageBar.fillTime").toFloat();
      if (abs(targetHealth - m_specialDamageBarValue) < fillSpeed * dt)
        m_specialDamageBarValue = targetHealth;
      else
        m_specialDamageBarValue += copysign(1.0f, targetHealth - m_specialDamageBarValue) * fillSpeed * dt;
    } else {
      m_specialDamageBarTarget = NullEntityId;
    }
  }

  if (m_specialDamageBarTarget == NullEntityId)
    m_specialDamageBarValue = 0.0f;

  if (m_specialDamageBarTarget == NullEntityId && m_client->mainPlayer()->inWorld()) {
    List<DamageBarEntityPtr> specialDamageTargets;
    m_client->worldClient()->forAllEntities([&specialDamageTargets](EntityPtr const& entity) {
        if (auto damageBarEntity = as<DamageBarEntity>(entity))
          if (damageBarEntity->damageBar() == DamageBarType::Special)
            specialDamageTargets.append(damageBarEntity);
      });
    sortByComputedValue(specialDamageTargets, [&](DamageBarEntityPtr entity) {
        return m_client->worldClient()->geometry().diff(entity->position(), m_client->mainPlayer()->position());
      });

    if (specialDamageTargets.size() > 0)
      m_specialDamageBarTarget = specialDamageTargets[0]->entityId();
  }

  for (auto const& message : m_client->mainPlayer()->pullQueuedMessages())
    queueMessage(message);

  auto chatHeight = (m_chat->active() && m_chat->visible() > 0.1) ? m_chat->size()[1] : 0;
  m_radioMessagePopup->setChatHeight(chatHeight);
  if (!m_cinematicOverlay || m_cinematicOverlay->completed()) {
    if (m_client->mainPlayer()->interruptRadioMessage())
      m_radioMessagePopup->interrupt();
    if (!m_radioMessagePopup->messageActive()) {
      if (auto radioMessage = m_client->mainPlayer()->pullPendingRadioMessage()) {
        m_radioMessagePopup->setMessage(*radioMessage);
        m_paneManager.displayRegisteredPane(MainInterfacePanes::RadioMessagePopup);
        ChatReceivedMessage message = {
          {MessageContext::RadioMessage},
          ServerConnectionId,
          Text::stripEscapeCodes(radioMessage->senderName),
          Text::stripEscapeCodes(radioMessage->text),
          Text::stripEscapeCodes(radioMessage->portraitImage.replace("<frame>", "0"))
        };
        m_chat->addMessages({message}, false);
      } else {
        m_paneManager.dismissRegisteredPane(MainInterfacePanes::RadioMessagePopup);
      }
    }

    m_client->mainPlayer()->setInCinematic(false);
  } else {
    m_client->mainPlayer()->setInCinematic(true);
  }

  for (auto const& drop : m_client->mainPlayer()->pullQueuedItemDrops())
    queueItemPickupText(drop);

  m_chat->addMessages(m_client->pullChatMessages());

  if (auto worldClient = m_client->worldClient()) {
    if (worldClient->inWorld()) {
      if (auto cinematic = m_client->mainPlayer()->pullPendingCinematic()) {
        if (*cinematic)
          m_cinematicOverlay->load(Root::singleton().assets()->fetchJson(cinematic.take()));
        else
          m_cinematicOverlay->stop();
      }
    }
  }

  if (!m_confirmationDialog->isDisplayed()) {
    if (auto confirmation = m_client->mainPlayer()->pullPendingConfirmation()) {
      m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation);
      m_confirmationDialog->displayConfirmation(confirmation->first, confirmation->second);
    }
  } else {
    auto confirmationSource = m_confirmationDialog->sourceEntityId();
    if (confirmationSource && !m_client->worldClient()->playerCanReachEntity(*confirmationSource))
      m_confirmationDialog->dismiss();
  }

  if (!m_joinRequestDialog->isDisplayed()) {
    if (auto req = m_queuedJoinRequests.maybeTakeLast()) {
      m_paneManager.displayRegisteredPane(MainInterfacePanes::JoinRequest);
      m_joinRequestDialog->displayRequest(req->first, [req](P2PJoinRequestReply reply) mutable {
          req->second.fulfill(reply);
        });
    }
  }

  for (EntityId id : m_interactionScriptPanes.keys()) {
    if (!m_paneManager.isDisplayed(m_interactionScriptPanes[id]))
      m_interactionScriptPanes.remove(id);
  }

  if (!m_messages.contains(m_overflowMessage))
    m_messageOverflow = 0;
  unsigned maxMessages = m_messageOverflow == 0 ? m_config->maxMessageCount : m_config->maxMessageCount + 1; // exclude overflow message
  if (m_messages.size() > maxMessages) {
    if (m_messageOverflow == 0) {
      m_messages.prepend(m_overflowMessage);
    }

    m_messageOverflow++;
    m_overflowMessage->message = m_config->overflowMessageText.replace("<count>", toString(m_messageOverflow));
    m_overflowMessage->cooldown = m_config->messageTime;
    if (auto oldest = m_messages.sorted([](GuiMessagePtr a, GuiMessagePtr b) { return a->cooldown < b->cooldown; }).maybeFirst())
      m_overflowMessage->cooldown = oldest.value()->cooldown;

    if (auto bottom = m_messages.filtered([this](GuiMessagePtr m) { return m != m_overflowMessage; }).maybeFirst())
      bottom.value()->cooldown = 0;
  }

  for (auto it = m_messages.begin(); it != m_messages.end();) {
    auto& message = *it;
    message->cooldown -= dt;
    if (message->cooldown < 0)
      it = m_messages.erase(it);
    else
      it++;
  }

  for (auto it = m_itemDropMessages.begin(); it != m_itemDropMessages.end();) {
    auto& message = *it;
    if (message.second.second->cooldown < 0)
      it = m_itemDropMessages.erase(it);
    else
      it++;
  }

  bool playerInWorld = m_client->mainPlayer()->inWorld();
  if (m_cinematicOverlay->completed()) {
    if (m_planetNameTimer.tick(dt)) {
      m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
    } else {
      if (playerInWorld) {
        String worldName;
        if (auto worldTemplate = m_client->worldClient()->currentTemplate())
          worldName = worldTemplate->worldName();

        if (!worldName.empty()) {
          m_planetText->setText(strf(m_config->planetNameFormatString.utf8Ptr(), worldName));
          m_paneManager.displayRegisteredPane(MainInterfacePanes::PlanetText);
        }
      }

      Color textColor = Color::White; // probably need to make this jsonable
      float fadeTimer = m_planetNameTimer.timer;
      if (fadeTimer < m_config->planetNameFadeTime)
        textColor.setAlphaF(fadeTimer / m_config->planetNameFadeTime);

      m_planetText->setColor(textColor);
    }
  } else if (!playerInWorld) {
    m_planetNameTimer.reset();
    m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText);
  }

  for (auto& containerResult : m_containerInteractor->pullContainerResults()) {
    if (!m_containerPane || !m_containerPane->giveContainerResult(containerResult)) {
      if (!m_inventoryWindow->giveContainerResult(containerResult)) {
        for (auto item : containerResult) {
          if (m_containerInteractor->containerOpen())
            m_containerInteractor->addToContainer(m_client->mainPlayer()->pickupItems(item));
          else
            m_client->mainPlayer()->giveItem(item);
        }
      }
    }
  }

  if (auto currentQuest = m_client->questManager()->currentQuest()) {
    m_paneManager.displayRegisteredPane(MainInterfacePanes::QuestTracker);
    m_questTracker->setQuest(*currentQuest);
  } else {
    m_paneManager.dismissRegisteredPane(MainInterfacePanes::QuestTracker);
  }

  updateCursor();

  m_nameplatePainter->update(dt, m_client->worldClient(), m_worldPainter->camera(), m_client->worldClient()->interactiveHighlightMode());
  m_questIndicatorPainter->update(dt, m_client->worldClient(), m_worldPainter->camera());

  m_chatBubbleManager->setCamera(m_worldPainter->camera());
  if (auto worldClient = m_client->worldClient()) {
    auto chatActions = worldClient->pullPendingChatActions();
    auto portraitActions = chatActions.filtered([](ChatAction action) { return action.is<PortraitChatAction>(); });

    for (auto action : portraitActions) {
      PortraitChatAction portraitAction = action.get<PortraitChatAction>();

      String name;
      if (auto npc = as<Npc>(worldClient->entity(portraitAction.entity)))
        name = npc->name();

      ChatReceivedMessage message = {
        { MessageContext::World },
        ServerConnectionId,
        Text::stripEscapeCodes(name),
        Text::stripEscapeCodes(portraitAction.text),
        Text::stripEscapeCodes(portraitAction.portrait.replace("<frame>", "0"))
      };
      m_chat->addMessages({message}, false);
    }

    m_chatBubbleManager->addChatActions(chatActions);
    m_chatBubbleManager->update(dt, worldClient);
  }

  if (auto container = m_client->worldClient()->get<ContainerEntity>(m_containerInteractor->openContainerId())) {
    if (!m_client->worldClient()->playerCanReachEntity(container->entityId())
        || !container->isInteractive())
      m_containerInteractor->closeContainer();
  }

  if (m_paneManager.topPane({PaneLayer::Window, PaneLayer::ModalWindow}))
    m_client->mainPlayer()->setBusyState(PlayerBusyState::Menu);
  else if (m_chat->hasFocus())
    m_client->mainPlayer()->setBusyState(PlayerBusyState::Chatting);
  else
    m_client->mainPlayer()->setBusyState(PlayerBusyState::None);

  for (auto& pair : m_canvases) {
    pair.second->setPosition(Vec2I());
    if (pair.second->ignoreInterfaceScale())
      pair.second->setSize(Vec2I(m_guiContext->windowSize()));
    else
      pair.second->setSize(Vec2I(m_guiContext->windowInterfaceSize()));
    pair.second->update(dt);
  }
}

void MainInterface::renderInWorldElements() {
  if (m_disableHud)
    return;

  m_guiContext->setDefaultFont();
  m_guiContext->setFontProcessingDirectives("");
  m_guiContext->setFontColor(Vec4B::filled(255));
  m_questIndicatorPainter->render();
  m_nameplatePainter->render();
  m_chatBubbleManager->render();
}

void MainInterface::render() {
  if (m_disableHud)
    return;

  m_guiContext->setDefaultFont();
  m_guiContext->setFontProcessingDirectives("");
  m_guiContext->setFontColor(Vec4B::filled(255));
  renderBreath();
  renderMessages();
  renderMonsterHealthBar();
  renderSpecialDamageBar();
  renderMainBar();
  renderDebug();

  RectI screenRect = RectI::withSize(Vec2I(), Vec2I(m_guiContext->windowSize()));
  for (auto& pair : m_canvases)
    pair.second->render(screenRect);

  renderWindows();
  renderCursor();
}

Vec2F MainInterface::cursorWorldPosition() const {
  return m_worldPainter->camera().screenToWorld(Vec2F(m_cursorScreenPos));
}

bool MainInterface::isDebugDisplayed() {
  return m_clientCommandProcessor->debugDisplayEnabled();
}

void MainInterface::doChat(String const& chat, bool addToHistory) {
  if (chat.empty())
    return;

  if (chat.beginsWith("/")) {
    m_lastCommand = chat;

    for (auto const& result : m_clientCommandProcessor->handleCommand(chat))
      m_chat->addLine(result);
  } else {
    m_client->sendChat(chat, m_chat->sendMode());
  }

  if (addToHistory)
    m_chat->addHistory(chat);
}

void MainInterface::queueMessage(String const& message, Maybe<float> cooldown, float spring) {
  auto guiMessage = make_shared<GuiMessage>(message, cooldown.value(m_config->messageTime), spring);
  m_messages.append(guiMessage);
}

void MainInterface::queueMessage(String const& message) {
  queueMessage(message, m_config->messageTime, 0.0f);
}

void MainInterface::queueJoinRequest(pair<String, RpcPromiseKeeper<P2PJoinRequestReply>> request)
{
  m_queuedJoinRequests.push_back(request);
}

void MainInterface::queueItemPickupText(ItemPtr const& item) {
  auto descriptor = item->descriptor();
  if (m_itemDropMessages.contains(descriptor.singular())) {
    auto countMessPair = m_itemDropMessages.get(descriptor.singular());
    auto newCount = item->count() + countMessPair.first;
    auto message = countMessPair.second;
    message->message = strf("{} - {}", item->friendlyName(), newCount);
    message->cooldown = m_config->messageTime;
    m_itemDropMessages[descriptor.singular()] = {newCount, message};
  } else {
    auto message = make_shared<GuiMessage>(strf("{} - {}", item->friendlyName(), item->count()), m_config->messageTime);
    m_messages.append(message);
    m_itemDropMessages[descriptor.singular()] = {item->count(), message};
  }
}

bool MainInterface::fixedCamera() const {
  return m_clientCommandProcessor->fixedCameraEnabled();
}

void MainInterface::warpToOrbitedWorld(bool deploy) {
  if (m_client->canBeamDown(deploy)) {
    if (deploy)
      m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "deploy", true);
    else
      m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "beam");
    return;
  }
  m_guiContext->playAudio("/sfx/interface/clickon_error.ogg");
}

void MainInterface::warpToOwnShip() {
  if (m_client->canBeamUp()) {
    warpTo(WarpAlias::OwnShip);
  } else {
    m_guiContext->playAudio("/sfx/interface/clickon_error.ogg");
  }
}

void MainInterface::warpTo(WarpAction const& warpAction) {
  if (m_client->beamUpRule() == BeamUpRule::AnywhereWithWarning) {
    if (m_confirmationDialog->isDisplayed())
      m_confirmationDialog->dismiss();

    m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation);
    m_confirmationDialog->displayConfirmation("/interface/windowconfig/beamupconfirmation.config", [this, warpAction] (Widget*) {
        m_client->warpPlayer(warpAction, true, "beam");
      }, [](Widget*) {});
  } else {
    m_client->warpPlayer(warpAction, true, "beam");
  }
}

CanvasWidgetPtr MainInterface::fetchCanvas(String const& canvasName, bool ignoreInterfaceScale) {
  CanvasWidgetPtr canvas;

  if (auto canvasPtr = m_canvases.ptr(canvasName))
    canvas = *canvasPtr;
  else {
    m_canvases.emplace(canvasName, canvas = make_shared<CanvasWidget>());
    canvas->setPosition(Vec2I());
    if (ignoreInterfaceScale)
      canvas->setSize(Vec2I(m_guiContext->windowSize()));
    else
      canvas->setSize(Vec2I(m_guiContext->windowInterfaceSize()));
  }

  canvas->setIgnoreInterfaceScale(ignoreInterfaceScale);
  return canvas;
}

// For when the player swaps characters. We need to completely reload ScriptPanes,
// because a lot of ScriptPanes do not expect the character to suddenly change and may break or spill data over.
void MainInterface::takeScriptPanes(List<ScriptPaneInfo>& out) {
  m_paneManager.dismissWhere([&](PanePtr const& pane) {
    if (auto scriptPane = as<ScriptPane>(pane)) {
      if (scriptPane->isDismissed())
        return false;
      auto sourceEntityId = scriptPane->sourceEntityId();
      m_interactionScriptPanes.remove(sourceEntityId);
      auto& info = out.emplaceAppend();
      info.scriptPane = scriptPane;
      info.config = scriptPane->rawConfig();
      info.sourceEntityId = sourceEntityId;
      info.visible = scriptPane->visibility();
      info.position = scriptPane->relativePosition();

      return true;
    }
    return false;
  });
}

void MainInterface::reviveScriptPanes(List<ScriptPaneInfo>& panes) {
  for (auto& info : panes) { // this is evil and stupid
    info.scriptPane->~ScriptPane();
    new(info.scriptPane.get()) ScriptPane(m_client, info.config, info.sourceEntityId);
    info.scriptPane->setVisibility(info.visible);
    displayScriptPane(info.scriptPane, info.sourceEntityId);
    info.scriptPane->setPosition(info.position);
  }
}

PanePtr MainInterface::createEscapeDialog() {
  auto assets = Root::singleton().assets();

  auto escapeDialog = make_shared<Pane>();
  auto escapeDialogPtr = escapeDialog.get();

  GuiReader escapeDialogReader;
  escapeDialogReader.registerCallback("returnToGame", [escapeDialogPtr](Widget*) {
      escapeDialogPtr->dismiss();
    });
  escapeDialogReader.registerCallback("showOptions", [escapeDialogPtr, this](Widget*) {
      escapeDialogPtr->dismiss();
      m_paneManager.displayRegisteredPane(MainInterfacePanes::Options);
    });
  escapeDialogReader.registerCallback("saveAndQuit", [escapeDialogPtr, this](Widget*) {
      m_state = ReturnToTitle;
      escapeDialogPtr->dismiss();
    });

  escapeDialogReader.construct(assets->json("/interface.config:escapeDialog"), escapeDialogPtr);
  escapeDialog->fetchChild<LabelWidget>("lblversion")->setText(strf("OpenStarbound - {} ({})", StarVersionString, StarArchitectureString));
  return escapeDialog;
}

float MainInterface::interfaceScale() const {
  return m_guiContext->interfaceScale();
}

unsigned MainInterface::windowHeight() const {
  return m_guiContext->windowHeight();
}

unsigned MainInterface::windowWidth() const {
  return m_guiContext->windowWidth();
}

Vec2I MainInterface::mainBarPosition() const {
  return Vec2I(windowWidth(), windowHeight()) - m_config->mainBarSize * interfaceScale();
}

void MainInterface::renderBreath() {
  auto assets = Root::singleton().assets();
  auto imgMetadata = Root::singleton().imageMetadataDatabase();

  Vec2I breathBarSize = Vec2I(Vec2F(m_guiContext->textureSize("/interface/breath/empty.png")) * interfaceScale());
  Vec2I breathOffset = jsonToVec2I(assets->json("/interface.config:breathPos"));

  Vec2F breathBackgroundCenterPos(windowWidth() * 0.5f + breathOffset[0] * interfaceScale(), windowHeight() - breathOffset[1] * interfaceScale());
  Vec2F breathBarPos = breathBackgroundCenterPos + Vec2F(jsonToVec2I(assets->json("/interface.config:breathBarPos")) * interfaceScale());

  float breath = m_client->mainPlayer()->breath();
  float breathMax = m_client->mainPlayer()->maxBreath();

  size_t blocks = round((10 * breath) / breathMax);

  if (blocks < 10) {
    String breathPath = "/interface/breath/breath.png";
    m_guiContext->drawQuad(breathPath, RectF::withCenter(breathBackgroundCenterPos, Vec2F(imgMetadata->imageSize(breathPath)) * interfaceScale()));
    for (size_t i = 0; i < 10; i++) {
      if (i >= blocks) {
        if (blocks == 0 && Time::monotonicMilliseconds() % 500 > 250)
          m_guiContext->drawQuad("/interface/breath/warning.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
        else
          m_guiContext->drawQuad("/interface/breath/empty.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
      } else {
        m_guiContext->drawQuad("/interface/breath/breathbar.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale());
      }
    }
  }
}

void MainInterface::renderMessages() {
  if (m_messages.empty())
    return;

  Vec2F totalOffset = {};
  auto imgMetadata = Root::singleton().imageMetadataDatabase();
  unsigned bottomOffset = Root::singleton().configuration()->getPath("inventory.bottomActionBar").optBool().value(false) ? 32 : 0;
  for (auto& message : m_messages) {
    Vec2F hiddenOffset = Vec2F(m_config->messageHiddenOffset);
    Vec2F activeOffset = Vec2F(m_config->messageActiveOffset);
    if (bottomOffset) {
      activeOffset[1] += bottomOffset;
      bottomOffset = 0;
    }
    Vec2F messageOffset = lerp(message->springState, Vec2F(), activeOffset - hiddenOffset);
    totalOffset += messageOffset;
    messageOffset = totalOffset + hiddenOffset;

    Vec2F backgroundCenterPos = Vec2F(windowWidth() * 0.5f + messageOffset[0] * interfaceScale(), messageOffset[1] * interfaceScale());

    Vec2F backgroundTextCenterPos = backgroundCenterPos + Vec2F(m_config->messageTextContainerOffset * interfaceScale());
    Vec2F messageTextOffset = backgroundTextCenterPos + Vec2F(m_config->messageTextOffset * interfaceScale());

    if (message->cooldown > m_config->messageHideTime)
      message->springState = (message->springState * m_config->messageWindowSpring + 1.0f) / (m_config->messageWindowSpring + 1.0f);
    else
      message->springState = (message->springState * m_config->messageWindowSpring) / (m_config->messageWindowSpring + 1.0f);

    m_guiContext->drawQuad(m_config->messageTextContainer,
        RectF::withCenter(backgroundTextCenterPos, Vec2F(imgMetadata->imageSize(m_config->messageTextContainer) * interfaceScale())));

    m_guiContext->setFont(m_config->font);
    m_guiContext->setFontSize(m_config->fontSize);
    m_guiContext->setFontColor(Color::White.toRgba());
    m_guiContext->renderText(message->message, {messageTextOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor});
  }
}

void MainInterface::renderMonsterHealthBar() {
  auto assets = Root::singleton().assets();
  auto imgMetadata = Root::singleton().imageMetadataDatabase();
  if (m_lastMouseoverTarget != NullEntityId && !m_stickyTargetingTimer.ready()) {
    auto world = m_client->worldClient();

    auto entity = world->entity(m_lastMouseoverTarget);
    auto showDamageEntity = as<DamageBarEntity>(entity);

    if (!showDamageEntity) {
      m_lastMouseoverTarget = NullEntityId;
      return;
    }

    Vec2F backgroundCenterPos = Vec2F(windowWidth() / 2.0f, windowHeight());

    auto container = assets->json("/interface.config:monsterHealth.container").toString();
    auto offset = jsonToVec2F(assets->json("/interface.config:monsterHealth.offset")) * interfaceScale();
    m_guiContext->drawQuad(container, RectF::withCenter(backgroundCenterPos + offset, Vec2F(imgMetadata->imageSize(container) * interfaceScale())));

    auto nameTextOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.nameTextOffset")) * interfaceScale();
    m_guiContext->setFont(m_config->font);
    m_guiContext->setFontSize(m_config->fontSize);
    m_guiContext->setFontColor(Color::White.toRgba());
    m_guiContext->renderText(showDamageEntity->name(), backgroundCenterPos + nameTextOffset);

    auto empty = assets->json("/interface.config:monsterHealth.progressEmpty").toString();
    auto filled = assets->json("/interface.config:monsterHealth.progressFilled").toString();
    auto progressBarOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.progressBarOffset")) * interfaceScale();
    auto chunks = assets->json("/interface.config:monsterHealth.progressChunks").toInt();
    int blocks = round(showDamageEntity->health() / showDamageEntity->maxHealth() * chunks);
    Vec2F barPos = backgroundCenterPos + progressBarOffset;
    Vec2F barItemOffset = Vec2F(imgMetadata->imageSize(filled)) * interfaceScale();
    barItemOffset[1] = 0;

    m_guiContext->drawQuad(empty, RectF::withSize(backgroundCenterPos + barPos, Vec2F(imgMetadata->imageSize(empty) * interfaceScale())));

    for (int i = 0; i < blocks; i++)
      m_guiContext->drawQuad(filled, barPos + barItemOffset * i, interfaceScale());

    auto portraitOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.portraitOffset")) * interfaceScale();
    auto portraitScale = assets->json("/interface.config:monsterHealth.portraitScale").toFloat() * interfaceScale();

    auto portraitScissorRect = jsonToRectF(assets->json("/interface.config:monsterHealth.portraitScissorRect")).scaled(interfaceScale());
    auto rect = portraitScissorRect.translated(backgroundCenterPos + portraitOffset);
    m_guiContext->setInterfaceScissorRect(RectI(RectF(rect).scaled(1.0f / interfaceScale())));
    auto portraitMaxSize = jsonToVec2I(assets->json("/interface.config:monsterHealth.portraitMaxSize"));
    List<Drawable> portrait = showDamageEntity->portrait(PortraitMode::Full);

    auto bounds = Drawable::boundBoxAll(portrait, true);
    if (m_portraitScale == 0)
      m_portraitScale = max<int>(1, ceil(max(bounds.size().x() / portraitMaxSize.x(), bounds.size().y() / portraitMaxSize.y())));
    Drawable::translateAll(portrait, {-bounds.xMin() - (bounds.width() * 0.5f), -bounds.yMin() }); // crop out whitespace, align bottom center
    Drawable::scaleAll(portrait, 1.0f / m_portraitScale);

    for (auto drawable : portrait)
      m_guiContext->drawDrawable(std::move(drawable), backgroundCenterPos + portraitOffset, portraitScale);

    m_guiContext->resetInterfaceScissorRect();
  }
}

void MainInterface::renderSpecialDamageBar() {
  if (m_specialDamageBarTarget == NullEntityId)
    return;

  auto assets = Root::singleton().assets();
  auto imgMetadata = Root::singleton().imageMetadataDatabase();

  if (auto target = as<DamageBarEntity>(m_client->worldClient()->entity(m_specialDamageBarTarget))) {
    Vec2F bottomCenter = Vec2F(windowWidth() / 2.0f, 0);

    auto barConfig = assets->json("/interface.config:specialDamageBar");

    auto background = barConfig.getString("background");
    auto backgroundOffset = jsonToVec2F(barConfig.get("backgroundOffset")) * interfaceScale();
    auto screenPos = RectF::withSize(bottomCenter + backgroundOffset, Vec2F(imgMetadata->imageSize(background) * interfaceScale()));
    m_guiContext->drawQuad(background, screenPos);

    auto fill = barConfig.getString("fill");
    auto fillOffset = jsonToVec2F(barConfig.get("fillOffset")) * interfaceScale();
    Vec2F size = Vec2F(barConfig.getInt("fillWidth") * m_specialDamageBarValue, imgMetadata->imageSize(fill).y());
    m_guiContext->drawQuad(fill, RectF::withSize(bottomCenter + fillOffset, size * interfaceScale()));

    auto nameOffset = jsonToVec2F(barConfig.get("nameOffset")) * interfaceScale();
    m_guiContext->setFontColor(jsonToColor(barConfig.get("nameColor")).toRgba());
    m_guiContext->setFontSize(barConfig.getUInt("nameSize"));
    m_guiContext->setFontProcessingDirectives(barConfig.getString("nameDirectives"));
    m_guiContext->renderText(target->name(), TextPositioning(bottomCenter + nameOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor));
    m_guiContext->setFontProcessingDirectives("");
  }
}

void MainInterface::renderMainBar() {
  Vec2I barPos = mainBarPosition();

  m_cursorTooltip = {};

  auto assets = Root::singleton().assets();

  Vec2I inventoryButtonPos = barPos + m_config->mainBarInventoryButtonOffset * interfaceScale();
  if (m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Inventory)) {
    if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) {
      m_guiContext->drawQuad(m_config->inventoryImageOpenHover, Vec2F(inventoryButtonPos), interfaceScale());
      m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString();
    } else {
      m_guiContext->drawQuad(m_config->inventoryImageOpen, Vec2F(inventoryButtonPos), interfaceScale());
    }
  } else if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) {
    if (m_inventoryWindow->containsNewItems())
      m_guiContext->drawQuad(m_config->inventoryImageGlowHover, Vec2F(inventoryButtonPos), interfaceScale());
    else
      m_guiContext->drawQuad(m_config->inventoryImageHover, Vec2F(inventoryButtonPos), interfaceScale());
    m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString();
  } else {
    if (m_inventoryWindow->containsNewItems())
      m_guiContext->drawQuad(m_config->inventoryImageGlow, Vec2F(inventoryButtonPos), interfaceScale());
    else
      m_guiContext->drawQuad(m_config->inventoryImage, Vec2F(inventoryButtonPos), interfaceScale());
  }

  auto drawStateButton = [this](MainInterfacePanes paneType, Vec2I pos, PolyI poly,
      String image, String hoverImage, String openImage, String hoverOpenImage, String toolTip) {
    if (m_paneManager.registeredPaneIsDisplayed(paneType)) {
      if (overButton(poly, m_cursorScreenPos)) {
        m_guiContext->drawQuad(hoverOpenImage, Vec2F(pos), interfaceScale());
        m_cursorTooltip = toolTip;
      } else {
        m_guiContext->drawQuad(openImage, Vec2F(pos), interfaceScale());
      }
    } else if (overButton(poly, m_cursorScreenPos)) {
      m_guiContext->drawQuad(hoverImage, Vec2F(pos), interfaceScale());
      m_cursorTooltip = toolTip;
    } else {
      m_guiContext->drawQuad(image, Vec2F(pos), interfaceScale());
    }
  };

  Vec2I craftButtonPos = barPos + m_config->mainBarCraftButtonOffset * interfaceScale();
  drawStateButton(MainInterfacePanes::CraftingPlain,
      craftButtonPos,
      m_config->mainBarCraftButtonPoly,
      m_config->craftImage,
      m_config->craftImageHover,
      m_config->craftImageOpen,
      m_config->craftImageOpenHover,
      assets->json("/interface.config:cursorTooltip.craftingText").toString());

  Vec2I codexButtonPos = barPos + m_config->mainBarCodexButtonOffset * interfaceScale();
  drawStateButton(MainInterfacePanes::Codex,
      codexButtonPos,
      m_config->mainBarCodexButtonPoly,
      m_config->codexImage,
      m_config->codexImageHover,
      m_config->codexImageOpen,
      m_config->codexImageHoverOpen,
      assets->json("/interface.config:cursorTooltip.codexText").toString());

  Vec2I mmUpgradeButtonPos = barPos + m_config->mainBarMmUpgradeButtonOffset * interfaceScale();
  if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe)) {
    drawStateButton(MainInterfacePanes::MmUpgrade,
        mmUpgradeButtonPos,
        m_config->mainBarMmUpgradeButtonPoly,
        m_config->mmUpgradeImage,
        m_config->mmUpgradeImageHover,
        m_config->mmUpgradeImageOpen,
        m_config->mmUpgradeImageHoverOpen,
        assets->json("/interface.config:cursorTooltip.mmUpgradeText").toString());
  } else {
    drawStateButton(MainInterfacePanes::MmUpgrade,
        mmUpgradeButtonPos,
        m_config->mainBarMmUpgradeButtonPoly,
        m_config->mmUpgradeImageDisabled,
        m_config->mmUpgradeImageDisabled,
        m_config->mmUpgradeImageDisabled,
        m_config->mmUpgradeImageDisabled,
        assets->json("/interface.config:cursorTooltip.disabledText").toString());
  }

  Vec2I collectionsButtonPos = barPos + m_config->mainBarCollectionsButtonOffset * interfaceScale();
  drawStateButton(MainInterfacePanes::Collections,
    collectionsButtonPos,
    m_config->mainBarCollectionsButtonPoly,
    m_config->collectionsImage,
    m_config->collectionsImageHover,
    m_config->collectionsImageOpen,
    m_config->collectionsImageHoverOpen,
    assets->json("/interface.config:cursorTooltip.collectionsText").toString());

  // when the player can't deploy or beam, show the deploy button disabled
  // when the player can beam up they can't deploy down, show beaming up button in deploy button's place
  // when the player can only deploy, only show deploy button
  // when the player can deploy or beam down, show both buttons

  Vec2F deployButtonPos(barPos + m_config->mainBarDeployButtonOffset * interfaceScale());
  if (m_client->canBeamUp()) {
    if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) {
      m_guiContext->drawQuad(m_config->beamUpImageHover, deployButtonPos, interfaceScale());
      m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamUpText").toString();
    } else {
      m_guiContext->drawQuad(m_config->beamUpImage, deployButtonPos, interfaceScale());
    }
  } else if (m_client->canBeamDown(true)) {
    if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) {
      m_guiContext->drawQuad(m_config->deployImageHover, deployButtonPos, interfaceScale());
      m_cursorTooltip = assets->json("/interface.config:cursorTooltip.deployText").toString();
    } else {
      m_guiContext->drawQuad(m_config->deployImage, deployButtonPos, interfaceScale());
    }
  } else {
    m_guiContext->drawQuad(m_config->deployImageDisabled, deployButtonPos, interfaceScale());
  }

  Vec2F beamButtonPos(barPos + m_config->mainBarBeamButtonOffset * interfaceScale());
  if (m_client->canBeamDown()) {
    if (overButton(m_config->mainBarBeamButtonPoly, m_cursorScreenPos)) {
      m_guiContext->drawQuad(m_config->beamDownImageHover, beamButtonPos, interfaceScale());
      m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamDownText").toString();
    } else {
      m_guiContext->drawQuad(m_config->beamDownImage, beamButtonPos, interfaceScale());
    }
  }

  Vec2I questLogButtonPos = barPos + m_config->mainBarQuestLogButtonOffset * interfaceScale();
  drawStateButton(MainInterfacePanes::QuestLog,
      questLogButtonPos,
      m_config->mainBarQuestLogButtonPoly,
      m_config->questLogImage,
      m_config->questLogImageHover,
      m_config->questLogImageOpen,
      m_config->questLogImageHoverOpen,
      assets->json("/interface.config:cursorTooltip.questsText").toString());
}

void MainInterface::renderWindows() {
  m_paneManager.render();
}

void MainInterface::renderDebug() {
  if (!isDebugDisplayed()) {
    SpatialLogger::clear();
    m_debugTextRect = RectF::null();
    LogMap::clear();
    SpatialLogger::setObserved(false);
    return;
  }
  SpatialLogger::setObserved(true);
  
  if (m_clientCommandProcessor->debugHudEnabled()) {
    auto assets = Root::singleton().assets();
    m_guiContext->setFontSize(m_config->debugFontSize);
    m_guiContext->setFont(m_config->debugFont);
    m_guiContext->setLineSpacing(0.5f);
    m_guiContext->setFontProcessingDirectives(m_config->debugFontDirectives);
    m_guiContext->setFontColor(Color::White.toRgba());
    m_guiContext->setFontMode(FontMode::Normal);

    bool clearMap = m_debugMapClearTimer.wrapTick();
    auto logMapValues = LogMap::getValues();
    if (clearMap)
      LogMap::clear();

    List<String> formatted;
    formatted.reserve(logMapValues.size());

    int counter = 0;
    for (auto const& pair : logMapValues) {
      TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * counter++) };
      String& text = formatted.emplace_back(strf("{}^lightgray;:^green,set; {}", pair.first, pair.second));
      m_debugTextRect.combine(m_guiContext->determineTextSize(text, positioning).padded(m_config->debugBackgroundPad));
    }

    if (!m_debugTextRect.isNull()) {
      RenderQuad& quad = m_guiContext->renderer()->immediatePrimitives()
        .emplace_back(std::in_place_type_t<RenderQuad>(), m_debugTextRect, m_config->debugBackgroundColor.toRgba(), 0.0f).get<RenderQuad>();

      quad.b.color[3] = quad.c.color[3] = 0;
    };

    m_debugTextRect = RectF::null();

    for (size_t index = 0; index != formatted.size(); ++index) {
      TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * index) };
      m_guiContext->renderText(formatted[index], positioning);
    }

    m_guiContext->setFontSize(8);
    m_guiContext->setDefaultFont();
    m_guiContext->setDefaultLineSpacing();
    m_guiContext->setFontColor(Vec4B::filled(255));
    m_guiContext->setFontProcessingDirectives("");
  }

  auto const& camera = m_worldPainter->camera();

  bool clearSpatial = m_debugSpatialClearTimer.wrapTick();

  for (auto const& line : SpatialLogger::getLines("world", clearSpatial)) {
    Vec2F begin = camera.worldToScreen(line.begin);
    Vec2F end = camera.worldGeometry().diff(line.end, line.begin) * camera.pixelRatio() * TilePixels + begin;
    m_guiContext->drawLine(begin, end, line.color, 1);
  }

  for (auto const& line : SpatialLogger::getLines("screen", clearSpatial))
    m_guiContext->drawLine(Vec2F(line.begin), Vec2F(line.end), line.color, 1);

  for (auto const& point : SpatialLogger::getPoints("world", clearSpatial)) {
    auto position = camera.worldToScreen(point.position);
    m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1);
  }

  for (auto const& point : SpatialLogger::getPoints("screen", clearSpatial)) {
    auto position = point.position;
    m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1);
    m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1);
  }

  m_guiContext->setFontSize(m_config->debugFontSize);

  for (auto const& logText : SpatialLogger::getText("world", clearSpatial)) {
    m_guiContext->setFontColor(logText.color);
    m_guiContext->renderText(logText.text.utf8Ptr(), camera.worldToScreen(logText.position));
  }

  for (auto const& logText : SpatialLogger::getText("screen", clearSpatial)) {
    m_guiContext->setFontColor(logText.color);
    m_guiContext->renderText(logText.text.utf8Ptr(), logText.position);
  }
  m_guiContext->setFontColor(Vec4B::filled(255));
}

void MainInterface::updateCursor() {
  Maybe<String> cursorOverride = m_actionBar->cursorOverride(m_cursorScreenPos);

  if (!cursorOverride) {
    if (auto pane = m_paneManager.getPaneAt(m_cursorScreenPos / interfaceScale())) {
      cursorOverride = cursorOverride.orMaybe(pane->cursorOverride(m_cursorScreenPos / interfaceScale()));
    } else {
      auto player = m_client->mainPlayer();
      if (auto anchorState = m_client->mainPlayer()->loungingIn()) {
        if (auto loungeable = m_client->worldClient()->get<LoungeableEntity>(anchorState->entityId)) {
          if (auto loungeAnchor = loungeable->loungeAnchor(anchorState->positionIndex))
            cursorOverride = cursorOverride.orMaybe(loungeAnchor->cursorOverride);
        }
      }
      if (!cursorOverride) {
        for (auto item : {player->primaryHandItem(), player->altHandItem()}) {
          if (auto activeItem = as<ActiveItem>(item)) {
            if (auto cursor = activeItem->cursor()) {
              cursorOverride = cursor;
              break;
            }
          } else if (auto inspectionTool = as<InspectionTool>(item)) {
            cursorOverride = String("/cursors/inspect.cursor");
            break;
          }
        }
      }
    }
  }

  if (cursorOverride)
    m_cursor.setCursor(cursorOverride.take());
  else
    m_cursor.resetCursor();
}

void MainInterface::renderCursor() {
  // if we're currently playing a cinematic, we should not render the mouse.
  if (m_cinematicOverlay && !m_cinematicOverlay->completed())
    return m_guiContext->applicationController()->setCursorVisible(false);

  Vec2I cursorPos = m_cursorScreenPos;
  Vec2I cursorSize = m_cursor.size();
  Vec2I cursorOffset = m_cursor.offset();
  unsigned int cursorScale = m_cursor.scale(interfaceScale());
  Drawable cursorDrawable = m_cursor.drawable();

  cursorPos[0] -= cursorOffset[0] * cursorScale;
  cursorPos[1] -= (cursorSize[1] - cursorOffset[1]) * cursorScale;
  if (!m_guiContext->trySetCursor(cursorDrawable, cursorOffset, cursorScale))
    m_guiContext->drawDrawable(cursorDrawable, Vec2F(cursorPos), cursorScale);

  if (m_cursorTooltip) {
    auto assets = Root::singleton().assets();
    auto imgDb = Root::singleton().imageMetadataDatabase();

    auto backgroundImage = assets->json("/interface.config:cursorTooltip.background").toString();
    auto rawCursorOffset = jsonToVec2I(assets->json("/interface.config:cursorTooltip.offset"));

    Vec2I tooltipSize = Vec2I(imgDb->imageSize(backgroundImage)) * interfaceScale();
    Vec2I cursorOffset = (Vec2I{0, -m_cursor.size().y()} + rawCursorOffset) * cursorScale;
    Vec2I tooltipOffset = m_cursorScreenPos + cursorOffset;
    size_t fontSize = assets->json("/interface.config:cursorTooltip.fontSize").toUInt();
    String font = assets->json("/interface.config:cursorTooltip.font").toString();
    Vec4B fontColor = jsonToColor(assets->json("/interface.config:cursorTooltip.color")).toRgba();

    m_guiContext->drawQuad(backgroundImage, Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), 0), interfaceScale());
    m_guiContext->setFontSize(fontSize);
    m_guiContext->setFontColor(fontColor);
    m_guiContext->setFont(font);
    m_guiContext->renderText(*m_cursorTooltip,
        TextPositioning(Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), tooltipSize.y()) / 2,
            HorizontalAnchor::HMidAnchor,
            VerticalAnchor::VMidAnchor));
  }

  m_cursorItem->setPosition(m_cursorScreenPos / interfaceScale() + m_config->inventoryItemMouseOffset);

  if (auto swapItem = m_client->mainPlayer()->inventory()->swapSlotItem())
    m_cursorItem->setItem(swapItem);
  else
    m_cursorItem->setItem({});

  m_cursorItem->render(RectI::withSize({}, {(int)windowWidth(), (int)windowHeight()}));
  m_guiContext->resetInterfaceScissorRect();
}

bool MainInterface::overButton(PolyI buttonPoly, Vec2I const& mousePos) const {
  Vec2I barPos = mainBarPosition();
  buttonPoly.translate(barPos);
  buttonPoly.scale(interfaceScale(), barPos);
  return buttonPoly.contains(mousePos);
}

bool MainInterface::overlayClick(Vec2I const& mousePos, MouseButton) {
  PolyI mainBarPoly = m_config->mainBarPoly;
  Vec2I barPos = mainBarPosition();
  mainBarPoly.translate(barPos);
  mainBarPoly.scale(interfaceScale(), barPos);

  if (overButton(m_config->mainBarInventoryButtonPoly, mousePos)) {
    m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory);
    return true;
  }

  if (overButton(m_config->mainBarCraftButtonPoly, mousePos)) {
    togglePlainCraftingWindow();
    return true;
  }

  if (overButton(m_config->mainBarCodexButtonPoly, mousePos)) {
    m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex);
    return true;
  }

  if (overButton(m_config->mainBarDeployButtonPoly, mousePos)) {
    if (m_client->canBeamDown(true))
      warpToOrbitedWorld(true);
    else if (m_client->canBeamUp())
      warpToOwnShip();
    return true;
  }

  if (overButton(m_config->mainBarBeamButtonPoly, mousePos)) {
    if (m_client->canBeamDown())
      warpToOrbitedWorld();
    return true;
  }

  if (overButton(m_config->mainBarQuestLogButtonPoly, mousePos)) {
    m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog);
    return true;
  }

  if (overButton(m_config->mainBarMmUpgradeButtonPoly, mousePos)) {
    if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe))
      m_paneManager.toggleRegisteredPane(MainInterfacePanes::MmUpgrade);
    return true;
  }

  if (overButton(m_config->mainBarCollectionsButtonPoly, mousePos)) {
    m_paneManager.toggleRegisteredPane(MainInterfacePanes::Collections);
    return true;
  }

  return false;
}

void MainInterface::displayScriptPane(ScriptPanePtr& scriptPane, EntityId sourceEntity) {
 // keep any number of script panes open with null source entities
  if (sourceEntity != NullEntityId)
    m_interactionScriptPanes[sourceEntity] = scriptPane;

  PaneLayer layer = PaneLayer::Window;
  if (auto layerName = scriptPane->config().optString("paneLayer"))
    layer = PaneLayerNames.getLeft(*layerName);

  if (scriptPane->openWithInventory()) {
    m_paneManager.displayPane(layer, scriptPane, [this](PanePtr const&) {
      if (auto player = m_client->mainPlayer())
        player->clearSwap();
      m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory);
    });
    m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory);
    m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory),
        scriptPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat());
  } else {
    m_paneManager.displayPane(layer, scriptPane);
  }
}

}