using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Scenes;
using Cthangover.Core.Utils;
using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming;
using Godot;
namespace Cthangover.Mods.Interface.View
{
///
/// Core viewport container: manages background/foreground TextureRects with
/// a shared shader material (timed_sprite from mods) for time-of-day lighting.
/// Supports smooth background transitions via a black ColorRect overlay that
/// fades in, swaps the texture mid-transition, then fades out — a classic
/// crossfade approach that doesn't require a second TextureRect. The zoom
/// system scales the content Control between minZoom and maxZoom based on
/// a 0–1 input value. Alignment positions content within the viewport using
/// ViewBoxNavigator's 9-point grid. The #if TOOLS block forces content size
/// to defaultResolution for editor preview.
///
/// Interactive layers are Control containers placed between Background and
/// Foreground (and above Foreground for UI) that host interactive scene objects.
///
public partial class ViewBox : Control, IBackgroundView
{
[Export] private Vector2I defaultResolution = new(1920, 1024);
[Export] private Control content;
[Export] private float minZoom;
[Export] private float maxZoom;
///
/// The content node that holds all scene visuals. Scaling and alignment are applied
/// directly to this node.
///
public Control Content => content;
/// Logical resolution of the content area (default 1920x1024). Used for normalised coordinate calculations.
public Vector2I LogicalSize => defaultResolution;
///
/// The background rendered behind all interactive layers.
/// Receives a timed_sprite shader material for time-of-day lighting when
/// no material is already assigned.
///
public TextureRect Background { get; private set; }
///
/// The foreground rendered above interactive layers.
/// Receives the same timed_sprite shader as the background.
///
public TextureRect Foreground { get; private set; }
/// Container for interactive objects rendered behind the foreground.
public Control InteractiveLayerBackground { get; private set; }
/// Container for interactive objects rendered above the foreground.
public Control InteractiveLayerForeground { get; private set; }
/// Container for interactive objects rendered above all scene visuals (UI layer).
public Control InteractiveLayerUI { get; private set; }
private ColorRect transitionOverlay;
private Tween transitionTween;
/// Resolves the background and foreground nodes and assigns the time-of-day shader when no material is set.
public override void _Ready()
{
Background ??= GetNodeOrNull("Background");
Foreground ??= GetNodeOrNull("Foreground");
var timedShader = Shaders.Resolve("timed_sprite");
if (timedShader != null)
{
if (Background != null && Background.Material == null)
{
Background.Material = new ShaderMaterial { Shader = timedShader };
GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Background", LogLevel.Debug);
}
if (Foreground != null && Foreground.Material == null)
{
Foreground.Material = new ShaderMaterial { Shader = timedShader };
GameLogger.Log("LIGHT", "ViewBox._Ready: created ShaderMaterial for Foreground", LogLevel.Debug);
}
}
SetZoom(0f);
SetAlign(AlignType.CenterCenter);
}
///
/// Returns the interactive layer container for the given logical layer name.
/// Creates the layer lazily as a direct child of ViewBox if it doesn't exist yet.
///
/// "background", "foreground" or "ui".
/// The matching Control container.
public Control GetInteractiveLayer(string layerName)
{
var result = layerName?.ToLowerInvariant() switch
{
"background" => InteractiveLayerBackground ??= CreateLayerDirect("InteractiveLayer_Background", 1),
"foreground" => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3),
"ui" => InteractiveLayerUI ??= CreateLayerDirect("InteractiveLayer_UI", 5),
_ => InteractiveLayerForeground ??= CreateLayerDirect("InteractiveLayer_Foreground", 3)
};
return result;
}
private Control CreateLayerDirect(string name, int zIndex)
{
var layer = new Control
{
Name = name,
MouseFilter = MouseFilterEnum.Ignore,
ZIndex = zIndex,
Size = new Vector2(defaultResolution.X, defaultResolution.Y)
};
AddChild(layer);
return layer;
}
///
/// Assigns a texture to the background . This is the
/// primary mechanism for scenes to set the backdrop visual. Logs the new background
/// ID from for debugging.
///
public void SetBackgroundTexture(Texture2D texture)
{
if (Background != null)
{
Background.Texture = texture;
GameLogger.Log("VIEW", $"set background '" + SceneContextNode.LastBackgroundID + "'");
}
}
///
/// Assigns a texture to the foreground . Typically used
/// for scene overlays rendered above interactive objects but below UI.
///
public void SetForegroundTexture(Texture2D texture)
{
if (Foreground != null)
{
Foreground.Texture = texture;
GameLogger.Log("VIEW", $"set foreground");
}
}
///
/// Returns the currently assigned background texture, or null if none is set
/// or the node is missing.
///
public Texture2D GetBackgroundTexture()
{
return Background?.Texture;
}
///
/// Smoothly transitions the background to a new texture using a black overlay crossfade:
/// fades a to full opacity over the first half of ,
/// swaps the background texture mid-transition, then fades back out over the second half.
/// If the target texture is already set or the background node is missing, no-op.
/// cancels any in-progress transition first.
///
/// The replacement background texture.
/// Total crossfade duration in seconds (default 0.5).
public void TransitionBackground(Texture2D newTexture)
{
TransitionBackground(newTexture, 0.5f);
}
public void TransitionBackground(Texture2D newTexture, float duration = 0.5f)
{
if (Background == null)
{
if (newTexture != null)
SetBackgroundTexture(newTexture);
return;
}
if (Background.Texture == newTexture)
return;
KillTransitionTween();
var overlay = GetOrCreateTransitionOverlay();
overlay.Visible = true;
overlay.Modulate = UiTokens.Alpha(Colors.Black, 0f);
overlay.MouseFilter = MouseFilterEnum.Ignore;
var halfDuration = duration / 2f;
transitionTween = CreateTween();
transitionTween.TweenProperty(overlay, "modulate:a", 1f, halfDuration);
transitionTween.TweenCallback(Callable.From(() =>
{
SetBackgroundTexture(newTexture);
}));
transitionTween.TweenProperty(overlay, "modulate:a", 0f, halfDuration);
transitionTween.TweenCallback(Callable.From(() =>
{
overlay.Visible = false;
transitionTween = null;
}));
}
///
/// Aborts any in-progress background transition by killing the tween and resetting
/// the overlay to invisible. Safe to call when no transition is active.
///
public void KillTransitionTween()
{
if (transitionTween != null)
{
transitionTween.Kill();
transitionTween = null;
}
if (transitionOverlay != null)
{
transitionOverlay.Visible = false;
transitionOverlay.Modulate = UiTokens.Alpha(Colors.Black, 0f);
}
}
private ColorRect GetOrCreateTransitionOverlay()
{
if (transitionOverlay == null)
{
transitionOverlay = new ColorRect
{
Name = "TransitionOverlay",
Color = UiTokens.Alpha(Colors.Black, 1f),
MouseFilter = MouseFilterEnum.Ignore,
Visible = false
};
transitionOverlay.AnchorRight = 1f;
transitionOverlay.AnchorBottom = 1f;
transitionOverlay.GrowHorizontal = GrowDirection.Both;
transitionOverlay.GrowVertical = GrowDirection.Both;
AddChild(transitionOverlay);
}
return transitionOverlay;
}
///
/// Applies a zoom level to the content node. The is a 0–1 lerp
/// factor between minZoom and maxZoom. Scale is uniform (X and Y are equal).
///
/// Normalized zoom factor: 0 = minZoom, 1 = maxZoom.
public void SetZoom(float value)
{
var zoom = minZoom + (maxZoom - minZoom) * value;
if (content != null)
content.Scale = Vector2.One * zoom;
}
///
/// Positions the content node within the viewport using the 9-point alignment grid
/// from .
///
/// Which corner/edge/center to align to.
public void SetAlign(AlignType align)
{
if (content != null)
content.Position = ViewBoxNavigator.GetPositionByAlign(align, content.Size);
}
#if TOOLS && DEBUG
/// Editor-only: forces the content back to the default resolution rect while inspecting properties.
public override void _ValidateProperty(Godot.Collections.Dictionary property)
{
base._ValidateProperty(property);
if (content != null)
content.Size = defaultResolution;
}
#endif
}
}