using System;
using System.Collections.Generic;
using Cthangover.Core.Mods;
using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming;
using Cthangover.Core.Scenes;
using Cthangover.Core.Utils;
using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Mods.ModManager
{
///
/// Single entry point of the mod UI (the manifest main_menu button
/// opens this hub instead of two separate screens). A slim header — back
/// button, title and a two-position segment control "Mods | Collections" —
/// hosts the child screens: the embedded
/// (default tab) and, on the collections tab, the
/// plus the scheme editor
/// as its second level. The selected
/// tab lives in a static field, so it survives closing and reopening the
/// hub within one game session.
///
/// The hub also carries the mockup's cross links: a collection link on a
/// mod card switches to the collections tab, a "not found" problem in the
/// editor jumps back to the mods tab with the id filled into the search
/// box (), and Escape walks back
/// editor → gallery → mods tab.
///
public partial class ModHubScreen : ModWidget
{
private const string TabMods = "mods";
private const string TabCollections = "collections";
private const string ModsScene = "scenes/mod_manager_screen.tscn";
private const string EditorScene = "scenes/mod_collections_screen.tscn";
/// Active tab, remembered for the whole game session (mockup: "state is remembered").
private static string _activeTab = TabMods;
private Button _back;
private Label _title;
private HBoxContainer _segment;
private Button _tabMods;
private Button _tabCollections;
private LineEdit _gallerySearch;
private Button _galleryRefresh;
private PanelContainer _bannerPanel;
private Label _bannerLabel;
private Control _body;
private ModManagerScreen _mods;
private CollectionsGallery _gallery;
private ModCollectionsScreen _editor;
private bool _editorOpen;
/// Mods installed this session, shared by the gallery and the editor so both stay honest before a restart.
private readonly HashSet _sessionInstalled = new(StringComparer.OrdinalIgnoreCase);
protected override void Construct()
{
MouseFilter = MouseFilterEnum.Stop;
CthangoverTheme.Apply(this);
BuildHeader();
BuildBanner();
BuildBody();
ShowTab(_activeTab);
}
/* ═══════════════ Header and body ═══════════════ */
private void BuildHeader()
{
var root = GetNode("Margin/Root");
root.AddThemeConstantOverride("separation", UiTokens.Space3);
var header = UiKit.Row(UiTokens.Space3);
root.AddChild(header);
_back = UiKit.Button(TranslationServer.Translate("mod_manager/hub_back_to_menu"));
_back.Pressed += OnBackPressed;
header.AddChild(_back);
_title = UiKit.Label(TranslationServer.Translate("mod_manager/menu_title"), UiTokens.FontLead, UiTokens.Ink);
_title.Uppercase = true;
header.AddChild(_title);
_segment = UiKit.Row(0);
var segmentPanel = new PanelContainer { MouseFilter = Control.MouseFilterEnum.Ignore };
segmentPanel.AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusSm, UiTokens.Well, UiTokens.Line));
segmentPanel.AddChild(_segment);
header.AddChild(segmentPanel);
var group = new ButtonGroup();
_tabMods = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_mods"), ButtonKind.Tab);
_tabMods.ToggleMode = true;
_tabMods.ButtonGroup = group;
_tabMods.Pressed += () => ShowTab(TabMods);
_segment.AddChild(_tabMods);
_tabCollections = UiKit.Button(TranslationServer.Translate("mod_manager/hub_tab_collections"), ButtonKind.Tab);
_tabCollections.ToggleMode = true;
_tabCollections.ButtonGroup = group;
_tabCollections.Pressed += () => ShowTab(TabCollections);
_segment.AddChild(_tabCollections);
header.AddChild(UiKit.Spacer());
_gallerySearch = new LineEdit
{
PlaceholderText = TranslationServer.Translate("mod_manager/gallery_search_placeholder"),
CustomMinimumSize = new Vector2(220, 0),
SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
};
_gallerySearch.TextChanged += OnGallerySearchChanged;
header.AddChild(_gallerySearch);
_galleryRefresh = UiKit.Button(TranslationServer.Translate("mod_manager/refresh"));
_galleryRefresh.Pressed += OnGalleryRefreshPressed;
header.AddChild(_galleryRefresh);
}
private void BuildBanner()
{
var root = GetNode("Margin/Root");
_bannerPanel = new PanelContainer { Visible = false };
_bannerPanel.AddThemeStyleboxOverride("panel", ModManagerTheme.MakeBox(
UiTokens.Alpha(UiTokens.Warning, 0.15f),
UiTokens.Alpha(UiTokens.Warning, 0.45f),
UiTokens.RadiusMd, UiTokens.Space4, UiTokens.Space2));
_bannerLabel = UiKit.Label(string.Empty, UiTokens.FontBodySmall, UiTokens.Warning);
_bannerLabel.HorizontalAlignment = HorizontalAlignment.Center;
_bannerPanel.AddChild(_bannerLabel);
root.AddChild(_bannerPanel);
}
private void BuildBody()
{
_body = new Control
{
SizeFlagsVertical = Control.SizeFlags.ExpandFill,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
GetNode("Margin/Root").AddChild(_body);
}
/* ═══════════════ Tabs and levels ═══════════════ */
private void ShowTab(string tab)
{
_activeTab = tab;
_editorOpen = false;
if (tab == TabMods)
{
EnsureMods();
_mods.Visible = true;
if (_gallery != null)
_gallery.Visible = false;
if (_editor != null)
_editor.Visible = false;
}
else
{
EnsureGallery();
_gallery.Visible = true;
if (_mods != null)
_mods.Visible = false;
if (_editor != null)
_editor.Visible = false;
}
UpdateHeader();
}
/// Leaves the editor back to the games gallery (crumbs, back button, Escape).
private void ShowGallery()
{
_editorOpen = false;
if (_editor != null)
_editor.Visible = false;
EnsureGallery();
_gallery.Visible = true;
UpdateHeader();
}
///
/// Deep link used by the main menu's game picker ("Configure"): shows
/// the collections tab with the scheme editor open on the game.
///
public void OpenCollectionEditor(string collectionId) => OpenEditor(collectionId, false);
/// Opens the editor on the second level of the collections tab.
private void OpenEditor(string collectionId, bool newGame)
{
EnsureGallery();
EnsureEditor();
_gallery.Visible = false;
if (_mods != null)
_mods.Visible = false;
_editor.Visible = true;
_editorOpen = true;
if (newGame)
_editor.OpenNewGame();
else
_editor.OpenCollection(collectionId);
UpdateHeader();
}
private void UpdateHeader()
{
if (_editorOpen && _editor != null)
{
_back.Text = TranslationServer.Translate("mod_manager/hub_back_to_gallery");
_back.TooltipText = _back.Text;
_title.Text = _editor.CurrentCollectionTitle ?? TranslationServer.Translate("mod_manager/collections_title");
_segment.Visible = false;
_gallerySearch.Visible = false;
_galleryRefresh.Visible = false;
return;
}
_back.Text = TranslationServer.Translate("mod_manager/hub_back_to_menu");
_back.TooltipText = _back.Text;
_title.Text = TranslationServer.Translate("mod_manager/menu_title");
_segment.Visible = true;
_tabMods.ButtonPressed = _activeTab == TabMods;
_tabCollections.ButtonPressed = _activeTab == TabCollections;
var collections = _activeTab == TabCollections;
_gallerySearch.Visible = collections;
_galleryRefresh.Visible = collections;
}
private void OnBackPressed()
{
if (_editorOpen)
ShowGallery();
else
QueueFree();
}
/* ═══════════════ Hosted screens ═══════════════ */
private void EnsureMods()
{
if (_mods != null)
return;
_mods = TscnScenes.LoadAndBuild(ModsScene) as ModManagerScreen;
if (_mods == null)
{
GameLogger.Log("MOD_HUB", $"Failed to build '{ModsScene}'", LogLevel.Error);
return;
}
_mods.Embedded = true;
_body.AddChild(_mods);
_mods.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_mods.EnsureConstructed();
_mods.CollectionsLinkRequested += () => ShowTab(TabCollections);
}
private void EnsureGallery()
{
if (_gallery != null)
return;
_gallery = new CollectionsGallery();
_gallery.SetSessionInstalled(_sessionInstalled);
_gallery.StatusMessage += ShowBanner;
_gallery.EditSchemeRequested += id => OpenEditor(id, newGame: false);
_gallery.NewGameRequested += () => OpenEditor(null, newGame: true);
_body.AddChild(_gallery);
_gallery.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_gallery.Refresh();
}
private void EnsureEditor()
{
if (_editor != null)
return;
_editor = TscnScenes.LoadAndBuild(EditorScene) as ModCollectionsScreen;
if (_editor == null)
{
GameLogger.Log("MOD_HUB", $"Failed to build '{EditorScene}'", LogLevel.Error);
return;
}
_editor.Embedded = true;
_editor.SetSessionInstalled(_sessionInstalled);
_editor.GalleryRequested += ShowGallery;
_editor.OpenManagerRequested += OnOpenManagerRequested;
_editor.ViewChanged += UpdateHeader;
_body.AddChild(_editor);
_editor.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
_editor.EnsureConstructed();
}
private void OnOpenManagerRequested(string searchText)
{
ShowTab(TabMods);
_mods?.SetSearch(searchText);
}
/* ═══════════════ Gallery header controls ═══════════════ */
private void OnGallerySearchChanged(string text)
{
_gallery?.ApplyFilter(text);
}
private async void OnGalleryRefreshPressed()
{
if (_galleryRefresh.Disabled)
return;
_galleryRefresh.Disabled = true;
ShowBanner(TranslationServer.Translate("mod_manager/loading"));
try
{
await CatalogService.RefreshAsync(forceRefresh: true);
}
catch (Exception ex)
{
GameLogger.Log("MOD_HUB", $"Catalog refresh failed: {ex.Message}", LogLevel.Warning);
}
if (!GodotObject.IsInstanceValid(this))
return;
_galleryRefresh.Disabled = false;
ShowBanner(null);
}
private void ShowBanner(string text)
{
if (!GodotObject.IsInstanceValid(_bannerPanel))
return;
_bannerPanel.Visible = !string.IsNullOrEmpty(text);
if (!string.IsNullOrEmpty(text))
_bannerLabel.Text = text;
}
/* ═══════════════ Escape chain ═══════════════ */
/// Escape walks back one level: editor → gallery → mods tab.
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("ui_cancel"))
return;
if (_editorOpen)
{
ShowGallery();
GetViewport().SetInputAsHandled();
}
else if (_activeTab == TabCollections)
{
ShowTab(TabMods);
GetViewport().SetInputAsHandled();
}
}
}
}