Editor/SubTerrainPaintSettings.cs
// Copyright (c) 2026 SubZero Studios LLC. All rights reserved.

using System;
using System.Collections.Generic;
using Sandbox;

namespace Editor.SubTerrain;

/// <summary>
/// Paint Texture settings. Ground is the default mode. UI labels live on the [Title]
/// attributes; OnlyOnSlope has no inspector title (Steep | All owns it).
/// </summary>
public class SubTerrainPaintSettings
{
	public const float WearMixMax = 0.40f;
	public const float PathMixMax = 0.36f;
	public const float DitherWarn = 0.36f;
	public const float PackCoreMin = 0.15f;
	public const float PackCoreMax = 0.95f;
	public const string LibraryVersion = "1.4.15";

	public SubTerrainPaintLayer Layer { get; set; } = SubTerrainPaintLayer.Base;

	public int MaterialIndex { get; set; }

	public float MixCap { get; set; } = 0.28f;

	/// <summary>Wear only. Move mix on the pair that is already there. The selected tmat is not written.</summary>
	[Property, Title( "Keep pair" ), WideMode]
	public bool MixOnly { get; set; }

	/// <summary>Alt-click the ground to stay on. Wear will not paint other bases.</summary>
	[Property, Title( "Stay on sampled" ), WideMode]
	public bool LimitToSampledGround { get; set; }

	/// <summary>Path only. Hard brush center packs the path tmat as base.</summary>
	[Property, Title( "Pack center" ), Description( "On: solid path core along the stroke, soft apron outside. Off: soft ground + path apron only." ), WideMode]
	public bool PackCenter { get; set; } = true;

	/// <summary>Path + Pack center. Higher = wider solid middle. Outer ring stays soft blend.</summary>
	[Property, Title( "Core" ), Description( "How much of the brush packs solid. Raise for a wider solid middle. Outer ring stays Center mix apron." ), Range( PackCoreMin, PackCoreMax ), Step( 0.01f ), WideMode]
	public float PackCore { get; set; } = 0.55f;

	[Property, Title( "How worn" ), Description( "Overlay mix. Default 0.28. Soft chew to 0.36; hard stop 0.40. Above 0.36 can dither." ), Range( 0.05f, WearMixMax ), Step( 0.01f ), WideMode]
	public float HowWorn
	{
		get => MixCap;
		set => MixCap = value;
	}

	[Property, Title( "Center mix" ), Description( "Soft apron overlay mix outside the packed core. Raise with brush Size for a wider blend." ), Range( 0.05f, PathMixMax ), Step( 0.01f ), WideMode]
	public float CenterMix
	{
		get => MixCap;
		set => MixCap = value;
	}

	public bool OnlyOnSlope { get; set; } = true;

	[Property, Title( "Steeper than" ), Description( "How steep the ground must be. 20 is a hill face. Lower it to catch gentler banks." ), Range( 1f, 60f ), Step( 1f ), WideMode]
	public float SlopeDegrees { get; set; } = 20f;

	public int SampledGroundIndex { get; set; } = -1;

	public int PathGroundIndex { get; set; } = -1;

	public bool HasSampledGround => SampledGroundIndex >= 0;

	public bool HasPathGround => PathGroundIndex >= 0;

	public bool Dithers => MixCap > DitherWarn;

	/// <summary>Stamp value above which Pack center writes solid path. Higher Core = lower threshold.</summary>
	public float PackThreshold
	{
		get
		{
			float core = Math.Clamp( PackCore, PackCoreMin, PackCoreMax );
			return Math.Clamp( 0.82f - core * 0.64f, 0.15f, 0.85f );
		}
	}

	public static float MixMaxFor( SubTerrainPaintLayer layer )
	{
		return layer == SubTerrainPaintLayer.Path ? PathMixMax : WearMixMax;
	}
}

/// <summary>
/// Per Ground / Wear / Path mix and gate, for the editor session.
/// </summary>
internal static class SubTerrainPaintStore
{
	struct LayerState
	{
		public float MixCap;
		public bool OnlyOnSlope;
		public float SlopeDegrees;
		public bool MixOnly;
		public bool LimitToSampledGround;
		public bool PackCenter;
		public float PackCore;
	}

	static readonly Dictionary<SubTerrainPaintLayer, LayerState> ByLayer = new();

	public static void Capture( SubTerrainPaintSettings settings )
	{
		if ( settings is null )
			return;

		ByLayer[settings.Layer] = Read( settings );
	}

	public static void Apply( SubTerrainPaintSettings settings, SubTerrainPaintLayer layer )
	{
		if ( settings is null )
			return;

		if ( !ByLayer.TryGetValue( layer, out var state ) )
			state = Defaults( layer );

		settings.Layer = layer;
		settings.MixCap = Math.Clamp( state.MixCap, 0.05f, SubTerrainPaintSettings.MixMaxFor( layer ) );
		settings.OnlyOnSlope = state.OnlyOnSlope;
		settings.SlopeDegrees = state.SlopeDegrees;
		settings.MixOnly = state.MixOnly;
		settings.LimitToSampledGround = state.LimitToSampledGround;
		settings.PackCenter = state.PackCenter;
		settings.PackCore = Math.Clamp( state.PackCore, SubTerrainPaintSettings.PackCoreMin, SubTerrainPaintSettings.PackCoreMax );
	}

	static LayerState Read( SubTerrainPaintSettings settings )
	{
		return new LayerState
		{
			MixCap = settings.MixCap,
			OnlyOnSlope = settings.OnlyOnSlope,
			SlopeDegrees = settings.SlopeDegrees,
			MixOnly = settings.MixOnly,
			LimitToSampledGround = settings.LimitToSampledGround,
			PackCenter = settings.PackCenter,
			PackCore = settings.PackCore
		};
	}

	static LayerState Defaults( SubTerrainPaintLayer layer )
	{
		if ( layer == SubTerrainPaintLayer.Path )
		{
			return new LayerState
			{
				MixCap = 0.36f,
				OnlyOnSlope = false,
				SlopeDegrees = 20f,
				PackCenter = true,
				PackCore = 0.55f
			};
		}

		if ( layer == SubTerrainPaintLayer.Overlay )
		{
			return new LayerState
			{
				MixCap = 0.28f,
				OnlyOnSlope = true,
				SlopeDegrees = 20f,
				PackCenter = true,
				PackCore = 0.55f
			};
		}

		return new LayerState
		{
			MixCap = 0.28f,
			OnlyOnSlope = false,
			SlopeDegrees = 20f,
			PackCenter = true,
			PackCore = 0.55f
		};
	}
}