/*
 * Copyright (c)2009-2025 DemiVision, LLC. All Rights Reserved. The information
 * herein is the CONFIDENTIAL and PROPRIETARY information of DemiVision, LLC.
 */

using System.Security.Cryptography;

using System.Text;
using System.Text.RegularExpressions;

namespace DXLib.Utils;

/*
 * Provides miscellaneous utility functions used throughout library.
 */
public static partial class DXUtils
{
	/* Reflection Utils */

	// Uses reflection to return specified field from given dynamic object
	public static object GetProperty( object obj, string field )
	{
		return obj?.GetType().GetProperty( field )?.GetValue( obj );
	}

	/* Version Utils */

	// Converts specified major.minor.micro version string to integer
	public static int VersionToInt( string version )
	{
		string parsed = version.Replace( ".", string.Empty );

		// Convert to int
		bool success = int.TryParse( parsed, out int result );

		return success ? result : -1;
	}

	// Converts specified integer version to major.minor.micro string
	public static string IntToVersion( int version )
	{
		string str = version.ToString();
		const string dot = ".";

		int len = str.Length;

		// 3101 -> 3.10.1
		return str.Insert( 1, dot ).Insert( len, dot );
	}

	/* List Utils */

	// Extension method for randomly shuffling specified object list
	public static void Shuffle<T>( this IList<T> list )
	{
		Random rnd = new();

		int n = list.Count;

		// Use Fisher-Yates algorithm
		while ( n > 1 )
		{
			n--;
			int k = rnd.Next( n + 1 );

                (list[n], list[k]) = (list[k], list[n]);
		}
	}

	/* String Utils */

	// Constants
	public const string Delimiter = ", ";

	// Creates comma-delimited string from specified list
	public static string Delimit( IList<string> list )
	{
		return string.Join( Delimiter, list );
	}

	// Returns specified string as Base64 encoded
	public static string ToBase64( string str )
	{
		byte[] encoded = Encoding.UTF8.GetBytes( str );

		return ToBase64( encoded );
	}

	// Return specified raw data as Base64 encoded string
	public static string ToBase64( byte[] data )
	{
		return Convert.ToBase64String( data );
	}

	// Returns specified raw data as UTF8 encoded string
	public static string ToUTF8( byte[] data )
	{
		return Encoding.UTF8.GetString( data );
	}

	// Converts specified string to raw UTF8 data
	public static byte[] ToBytes( string str )
	{
		return Encoding.UTF8.GetBytes( str );
	}

	// Returns specified string with first character capitalized
	public static string ToFirstUpper( string str )
	{
		// Safety check
		if ( string.IsNullOrEmpty( str ) )
		{
			return null;
		}

		// Fastest implementation
		char[] chars = str.ToCharArray();
		chars[0] = char.ToUpper( chars[0] );

		return new string( chars );
	}

	// Returns specified string with all space characters removed
	public static string RemoveSpaces( string str )
	{
		return str?.Replace( " ", string.Empty );
	}

	// Returns string in canonical format
	public static string ToCanonical( string str, bool numeric = true )
	{
		if ( str != null )
		{
			string canonical = str;

			// Remove all whitespace
			canonical = RemoveWhitespace().Replace(canonical, string.Empty);

			// Remove all special characters (except '_')
			canonical = RemoveSpecialChars().Replace(canonical, string.Empty);

			// Optionally remove numerics
			if ( !numeric )
			{
				canonical = RemoveNumerics().Replace(canonical, string.Empty);
			}

			// Convert to lowercase
			return canonical.ToLower();
		}

		return null;
	}

	// Returns string in canonical format suitable for use in filenames
	public static string ToFilename( string str )
	{
		return ToCanonical( str );
	}

	// Counts number occurrences of test string in source string
	public static int Count( string str, string test )
	{
		return str.Length - str.Replace( test, string.Empty ).Length;
	}

	// Shortens specified string to given length, only if necessary
	public static string Truncate( string text, int length )
	{
		if ( string.IsNullOrEmpty( text ) || text.Length <= length )
		{
			return text;
		}

		return text[ ..length ];
	}

	// Converts specified text value to integer, null if invalid
	public static int? ConvertToInt( string text )
	{
		return IsValidNumber( text ) ? Convert.ToInt32( text ) : null;
	}

	// Determines whether specified text value is valid positive integer
	public static bool IsValidNumber( string text )
	{
		// Must have value
		if ( string.IsNullOrEmpty( text ) )
		{
			return false;
		}

		int number;

		// Parse integer
		try
		{
			number = Convert.ToInt32( text );
		}
		catch ( Exception )
		{
			return false;
		}

		// Must be positive
		return number >= 0;
	}

	// Computes MD5 hashed checksum based on specified input string
	public static byte[] ComputeChecksum( string input )
    {
		// Convert to byte array
        byte[] bytes = new UTF8Encoding().GetBytes(input);

        // Compute MD5 hash
        return MD5.HashData(bytes);
	}

    // Returns plural form of specified word (English only)
    public static string Pluralize( string word )
	{
		return word.EndsWith( "ch" ) ? $"{word}es" : $"{word}s";
	}

	/* Layout Utils */

	// Creates a padding thickness with only specified left padding
	public static Thickness Left( double value )
	{
		return new Thickness( value, 0, 0, 0 );
	}

	// Creates a padding thickness with only specified right padding
	public static Thickness Right( double value )
	{
		return new Thickness( 0, 0, value, 0 );
	}

	// Creates a padding thickness with only specified top padding
	public static Thickness Top( double value )
	{
		return new Thickness( 0, value, 0, 0 );
	}

	// Creates a padding thickness with only specified bottom padding
	public static Thickness Bottom( double value )
	{
		return new Thickness( 0, 0, 0, value );
	}

	// Adds additional padding to an existing thickness object
	public static Thickness AddPadding( Thickness value1, Thickness value2 )
	{
		return new Thickness( (value1.Left + value2.Left), (value1.Top + value2.Top), (value1.Right + value2.Right), (value1.Bottom + value2.Bottom) );
	}
	
	// Returns a new thickness object with specified additional padding
	public static Thickness AddPadding( Thickness padding, double addLeft, double addTop, double addRight, double addBottom )
	{
		return new Thickness( (padding.Left + addLeft), (padding.Top + addTop), (padding.Right + addRight), (padding.Bottom + addBottom) );
	}

	/* Date/Time Utils */

	// Returns current wall clock time
	public static DateTimeOffset Now()
	{
		return DateTimeOffset.Now;
	}

	// Determines if specified date is today
	public static bool IsToday( DateTimeOffset date )
	{
		return date.Date == DateTime.Today.Date;
	}
	
	// Converts specified date to string in format 'Friday, August 9' or 'Friday, Aug 9'
	public static string DayFromDate( DateTimeOffset date, bool abbrev = false )
	{
		return date.LocalDateTime.ToString( abbrev ? "dddd, MMM d" : "dddd, MMMM d" );
	}

	// Converts specified date to string in format 'August 6, 1971' or 'August 6'
	public static string MonthFromDate( DateTimeOffset date, bool year = true )
	{
		return date.LocalDateTime.ToString( year ? "MMMM d, yyyy" : "MMMM d" );
	}

	// Converts specified date to string in format 'August 6 1:23pm'
	public static string MonthTimeFromDate( DateTimeOffset date )
	{
		return date.LocalDateTime.ToString( "MMMM d h:mmtt" );
	}

	// Converts specified date to string in format '9/10/19'
	public static string LabelFromDate( DateTimeOffset date, bool year = true )
	{
		return date.LocalDateTime.ToString( year ? "M/dd/yy" : "M/dd" );
	}

	// Converts specified date to string in format '190910' for use in filenames
	public static string FilenameFromDate( DateTimeOffset date )
	{
		return date.LocalDateTime.ToString( "yyMMdd" );
	}

	// Converts specified date to ISO 8601 format
	public static string ISOFromDate( DateTimeOffset date )
	{
		return date.ToUniversalTime().ToString( "u" ).Replace( " ", "T" );
	}

	// Converts specified date to timestamp string in format 'yyyy-MM-dd HH:mm'
	public static string TimestampFromDate( DateTimeOffset date )
	{
		return date.LocalDateTime.ToString( "yyyy-MM-dd HH:mm" );
	}

	// Converts specified date to time string in format '1:23pm'
	public static string TimeFromDate( DateTimeOffset time )
	{
		return TimeFromDate( time.LocalDateTime );
	}

	// Converts specified date to time string in format '1:23pm'
	public static string TimeFromDate( DateTime time )
	{
		return time.ToString( "h:mmtt" ).ToLower();
	}

	// Returns string representing date range between two specified dates
	public static string DateRange( DateTimeOffset date1, DateTimeOffset date2 )
	{
		string str1, str2;

		if ( date1.Year == date2.Year )
		{
			// 'August 6 - 8, 1971'
			if ( date1.Month == date2.Month )
			{
				str1 = date1.LocalDateTime.ToString( "MMMM d" );
				str2 = date2.LocalDateTime.ToString( "d, yyyy" );
			}
			// 'Aug 6 - Sep 8, 1971'
			else
			{
				str1 = date1.LocalDateTime.ToString( "MMM d" );
				str2 = date2.LocalDateTime.ToString( "MMM d, yyyy" );
			}
		}
		else
		{
			// 'Aug 6, 1971 - Sep 8, 1972'
			str1 = date1.LocalDateTime.ToString( "MMM d, yyyy" );
			str2 = date2.LocalDateTime.ToString( "MMM d, yyyy" );
		}

		return $"{str1} - {str2}";
	}

	/* Elapsed Time Utils */

	// Returns elapsed time (ms) since specified start time
	public static int ElapsedTime( DateTimeOffset start )
	{
		return (int) DateTimeOffset.Now.Subtract( start ).TotalMilliseconds;
	}

	// Converts specified elapsed time (ms) to string in format '2hrs 30min 15.1sec'
	public static string FromDuration( int millis, bool precise = false )
	{
		return FromDuration( TimeSpan.FromMilliseconds( millis ), precise );
	}

	// Converts specified elapsed time to string in format '2hr 30min 15.1sec'
	public static string FromDuration( TimeSpan duration, bool precise = false )
	{
		string sec = DXString.Get( "form.time.sec" );
		string min = DXString.Get( "form.time.min" );

		double seconds = duration.TotalMilliseconds / 1000.0;

		// '15.1s'
		string format = precise ? $"{seconds:N1}{sec}" : $"{duration.Seconds:N0}{sec}";

		// '30m 15.1s'
		if ( duration.TotalMinutes > 1 )
		{
			format = $"{duration.Minutes}{min} {format}";
		}

		// '2h 30m 15.1s'
		if ( duration.TotalHours > 1 )
		{
			string hr = DXString.Get( "form.time.hour" );

			// Only include seconds for precise format
			format = precise ? $"{duration.Hours}{hr} {format}" : $"{duration.Hours}{hr} {duration.Minutes}{min}";
		}

		return format;
	}

	// Converts specified elapsed time to string in format 'hr:min:sec'
	public static string FromDurationShort( TimeSpan duration )
	{
		// Only include 'hr:' if necessary
		return (duration.TotalHours > 1) ? $"{duration.Hours}:{duration.Minutes:D2}:{duration.Seconds:D2}" 
										 : $"{duration.Minutes}:{duration.Seconds:D2}";
	}

	// Converts specified elapsed time (ms) to string in format 'hr:min:sec'
	public static string FromDurationShort( int duration )
	{
		return FromDurationShort( TimeSpan.FromMilliseconds( duration ) );
	}

	// Returns time in decimal hours (e.g., 12:30pm = 14.5 hours)
	public static double HoursFromDate( DateTimeOffset date )
	{
		DateTime local = date.LocalDateTime;
		int minutes = local.Minute;

		return local.Hour + minutes / 60.0;
	}

	// Rounds specified time up to nearest 15min interval
	public static DateTime RoundToTime( DateTimeOffset date, int minutes )
	{
		DateTime local = date.LocalDateTime;
		TimeSpan time = TimeSpan.FromMinutes( minutes );

		return new DateTime( (local.Ticks + time.Ticks - 1) / time.Ticks * time.Ticks, local.Kind );
	}

	// Converts DateTime to DateTimeOffset while preserving local timezone
	public static DateTimeOffset LocalDate( DateTime date )
	{
		return (DateTimeOffset) DateTime.SpecifyKind( date, DateTimeKind.Local );
	}

	/* RegEx */

	// Compile time RegEx to remove whitespace from string
	[GeneratedRegex("\\s+")]
	private static partial Regex RemoveWhitespace();

	// Compile time RegEx to remove special characters from string
	[GeneratedRegex("[*'\",&#^@]")]
	private static partial Regex RemoveSpecialChars();

	// Compile time RegEx to remove numeric characters from string
	[GeneratedRegex("[\\d-]")]
	private static partial Regex RemoveNumerics();
}

//
