Editor/HotCodeEditor/Settings/HotCodeSettings.cs
using Sandbox;
using Editor;
using System;

public enum AutoSaveMode
{
	[Title( "Off" )] Off,
	[Title( "After a short pause" )] AfterDelay,
	[Title( "When switching tabs" )] WhenSwitchingTabs
}

public enum CodeTheme
{
	[Title( "Dark+ (VS Code)" )] DarkPlus,
	[Title( "Monokai" )] Monokai,
	[Title( "One Dark" )] OneDark
}

/// <summary>
/// HotCode's settings. Static properties so s&amp;box's ControlSheet can edit them directly, the same
/// way its own Editor Settings pages work. Saved as one editor cookie; <see cref="Version"/> goes up on
/// every change so open editors know to re-measure and re-layout.
/// </summary>
public static class HotCodeSettings
{
	const string CookieKey = "hotcode.settings";

	/// <summary>
	/// The stored values. A plain class so it round-trips through JSON.
	/// </summary>
	public class Data
	{
		public float FontSize { get; set; } = 10;
		public string FontFamily { get; set; } = "";
		public float LineSpacing { get; set; } = 1.35f;
		public int TabSize { get; set; } = 4;
		public bool IndentWithSpaces { get; set; }
		public bool WordWrap { get; set; }
		public bool ShowLineNumbers { get; set; } = true;
		public bool HighlightCurrentLine { get; set; } = true;
		public bool CursorBlink { get; set; } = true;
		public CodeTheme Theme { get; set; } = CodeTheme.DarkPlus;

		public bool AutoCloseBrackets { get; set; } = true;
		public bool SuggestWhileTyping { get; set; } = true;
		public bool ParameterHints { get; set; } = true;
		public bool UnderlineErrors { get; set; } = true;

		public AutoSaveMode AutoSave { get; set; } = AutoSaveMode.Off;
		public float AutoSaveDelay { get; set; } = 1.5f;
		public bool TrimTrailingWhitespace { get; set; }
		public bool InsertFinalNewline { get; set; }
		public bool ReloadChangedFiles { get; set; } = true;
	}

	private static Data _data;

	private static Data Current
	{
		get
		{
			if ( _data is not null ) return _data;

			_data = EditorCookie.Get<Data>( CookieKey, null );
			if ( _data is null )
			{
				// First run: carry over the settings that used to be stored on their own
				_data = new Data
				{
					FontSize = EditorCookie.Get( "hotcode.fontsize", 10f ),
					WordWrap = EditorCookie.Get( "hotcode.wordwrap", false )
				};
			}

			Apply();
			return _data;
		}
	}

	/// <summary>
	/// Goes up every time any setting changes.
	/// </summary>
	public static int Version { get; private set; }

	private static void Set( Action<Data> change )
	{
		change( Current );
		EditorCookie.Set( CookieKey, Current );
		Apply();
		Version++;
	}

	/// <summary>
	/// Pushes settings into the places that read them as plain values.
	/// </summary>
	private static void Apply()
	{
		TextLayout.TabSize = Math.Clamp( _data.TabSize, 1, 16 );
		SyntaxColors.Use( _data.Theme );
	}

	public static void ResetToDefaults() => Set( d =>
	{
		var defaults = new Data();
		foreach ( var property in typeof( Data ).GetProperties() )
			property.SetValue( d, property.GetValue( defaults ) );
	} );

	// --- EDITOR ---

	[Title( "Font Size" ), Description( "Code font size in points. Ctrl+= / Ctrl+- or Ctrl+mouse wheel change it too." ), Range( 6, 28 ), Step( 1 )]
	public static float FontSize { get => Current.FontSize; set => Set( d => d.FontSize = Math.Clamp( value, 6, 28 ) ); }

	[Title( "Font" ), Description( "Code font family. Leave empty to use s&box's monospace font. Use a monospaced font so columns line up." )]
	public static string FontFamily { get => Current.FontFamily; set => Set( d => d.FontFamily = value?.Trim() ?? "" ); }

	[Title( "Line Spacing" ), Description( "Line height as a multiple of the font height." ), Range( 1, 2 ), Step( 0.05f )]
	public static float LineSpacing { get => Current.LineSpacing; set => Set( d => d.LineSpacing = Math.Clamp( value, 1, 2 ) ); }

	[Title( "Tab Size" ), Description( "How many columns a tab takes up." ), Range( 1, 8 ), Step( 1 )]
	public static int TabSize { get => Current.TabSize; set => Set( d => d.TabSize = Math.Clamp( value, 1, 8 ) ); }

	[Title( "Indent With Spaces" ), Description( "For files whose indentation can't be detected. Existing files keep whatever they already use." )]
	public static bool IndentWithSpaces { get => Current.IndentWithSpaces; set => Set( d => d.IndentWithSpaces = value ); }

	[Title( "Word Wrap" ), Description( "Wrap long lines to the width of the editor (Alt+Z)." )]
	public static bool WordWrap { get => Current.WordWrap; set => Set( d => d.WordWrap = value ); }

	[Title( "Line Numbers" )]
	public static bool ShowLineNumbers { get => Current.ShowLineNumbers; set => Set( d => d.ShowLineNumbers = value ); }

	[Title( "Highlight Current Line" )]
	public static bool HighlightCurrentLine { get => Current.HighlightCurrentLine; set => Set( d => d.HighlightCurrentLine = value ); }

	[Title( "Blinking Cursor" )]
	public static bool CursorBlink { get => Current.CursorBlink; set => Set( d => d.CursorBlink = value ); }

	[Title( "Code Colours" ), Description( "Syntax colours for the code area. The rest of HotCode follows s&box's theme." )]
	public static CodeTheme CodeColors { get => Current.Theme; set => Set( d => d.Theme = value ); }

	// --- CODE HELP ---

	[Title( "Auto-Close Brackets and Quotes" ), Description( "Typing ( [ { \" or ' adds the closing one." )]
	public static bool AutoCloseBrackets { get => Current.AutoCloseBrackets; set => Set( d => d.AutoCloseBrackets = value ); }

	[Title( "Suggestions While Typing" ), Description( "Show autocomplete as you type. Ctrl+Space always works." )]
	public static bool SuggestWhileTyping { get => Current.SuggestWhileTyping; set => Set( d => d.SuggestWhileTyping = value ); }

	[Title( "Parameter Hints" ), Description( "Show a method's parameters when typing ( or , in a call. Ctrl+Shift+Space always works." )]
	public static bool ParameterHints { get => Current.ParameterHints; set => Set( d => d.ParameterHints = value ); }

	[Title( "Underline Compile Errors" ), Description( "Underline errors and warnings from s&box's last compile in the code." )]
	public static bool UnderlineErrors { get => Current.UnderlineErrors; set => Set( d => d.UnderlineErrors = value ); }

	// --- FILES ---

	[Title( "Auto Save" ), Description( "Saving triggers s&box's hotload, so auto save means changes show up in the game as you type." )]
	public static AutoSaveMode AutoSave { get => Current.AutoSave; set => Set( d => d.AutoSave = value ); }

	[Title( "Auto Save Delay" ), Description( "Seconds after you stop typing, when Auto Save is \"After a short pause\"." ), Range( 0.5f, 10 ), Step( 0.5f )]
	public static float AutoSaveDelay { get => Current.AutoSaveDelay; set => Set( d => d.AutoSaveDelay = Math.Clamp( value, 0.5f, 10 ) ); }

	[Title( "Trim Trailing Whitespace" ), Description( "Remove spaces and tabs at the ends of lines when saving." )]
	public static bool TrimTrailingWhitespace { get => Current.TrimTrailingWhitespace; set => Set( d => d.TrimTrailingWhitespace = value ); }

	[Title( "Insert Final Newline" ), Description( "Make sure files end with a newline when saving." )]
	public static bool InsertFinalNewline { get => Current.InsertFinalNewline; set => Set( d => d.InsertFinalNewline = value ); }

	[Title( "Reload Files Changed Elsewhere" ), Description( "When another program changes an open file that has no unsaved edits here, reload it." )]
	public static bool ReloadChangedFiles { get => Current.ReloadChangedFiles; set => Set( d => d.ReloadChangedFiles = value ); }

	/// <summary>
	/// The code font to use: the chosen family, or s&amp;box's monospace font.
	/// </summary>
	public static string CodeFont => string.IsNullOrWhiteSpace( FontFamily ) ? Editor.Theme.MonospaceFont : FontFamily;
}