IconifyIcon.cs
using Iconify;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;

namespace Sandbox.UI;

public struct IconifyIcon
{
	public const int CurrentVersion = 1;

	public string Prefix { get; private set; }
	public string Name { get; private set; }

	public bool IsTintable { get; private set; }

	private readonly string Url => $"https://api.iconify.design/{Prefix}/{Name}.svg?width=100%";
	private readonly string LocalPath => $"{Prefix}/{Name}.svg";
	private readonly string MetadataPath => $"{Prefix}/{Name}.json";

	private async Task<string> FetchImageDataAsync()
	{
		var response = await Http.RequestAsync( Url, "GET" );
		var iconContents = await response.Content.ReadAsStringAsync();

		Log.Trace( Url );

		// this API doesn't actually return a 404 status code :( check the document for '404' itself...
		if ( response.StatusCode == HttpStatusCode.NotFound || iconContents == "404" )
			throw new Exception( $"Failed to fetch icon {this}" );
		
		return iconContents;
	}

	private string FetchMetadata()
	{
		var metadata = new IconMetadata()
		{
			Version = CurrentVersion,
			TimeFetched = DateTime.Now
		};

		return Json.Serialize( metadata );
	}

	public async Task EnsureIconDataIsCachedAsync( BaseFileSystem fs )
	{
		bool shouldFetch = !fs.FileExists( LocalPath );
		
		if ( fs.FileExists( MetadataPath ) )
		{
			var metadata = fs.ReadJson<IconMetadata>( MetadataPath );
			shouldFetch &= (metadata.Version != CurrentVersion);
		}
		else
		{
			shouldFetch = true;
		}

		if ( shouldFetch )
		{
			var directory = Path.GetDirectoryName( LocalPath );
			fs.CreateDirectory( directory );

			var iconContents = await FetchImageDataAsync();
			fs.WriteAllText( LocalPath, iconContents );

			var metadataContents = FetchMetadata();
			fs.WriteAllText( MetadataPath, metadataContents );
		}
	}

	public async Task<Texture> LoadTextureAsync( Rect rect, Color? tintColor )
	{
		var fs = IconifyOptions.Current.CacheFileSystem;
		await EnsureIconDataIsCachedAsync( fs );

		// HACK: Check whether this icon is tintable based on whether it references CSS currentColor
		var imageData = await fs.ReadAllTextAsync( LocalPath );
		IsTintable = imageData.Contains( "currentColor" );

		var pathParams = BuildPathParams( rect, tintColor );
		var path = LocalPath + pathParams;
    
		return Texture.Load( fs, path );
	}

	private string BuildPathParams( Rect rect, Color? tintColor )
	{
		var pathParamsBuilder = new StringBuilder( "?" );

		if ( IsTintable && tintColor.HasValue )
			pathParamsBuilder.Append( $"color={tintColor.Value.Hex}&" );

		var width = Math.Max( 32, rect.Width );
		var height = Math.Max( 32, rect.Height );

		pathParamsBuilder.Append( $"w={width}&h={height}" );
		return pathParamsBuilder.ToString();
	}

	public IconifyIcon( string path )
	{
		if ( !path.Contains( ':' ) )
			throw new ArgumentException( $"Icon must be in the format 'prefix:name', got '{path}'" );

		var splitName = path.Split( ':', StringSplitOptions.RemoveEmptyEntries );

		if ( splitName.Length != 2 )
			throw new ArgumentException( $"Icon must be in the format 'prefix:name', got '{path}'" );

		Prefix = splitName[0].Trim();
		Name = splitName[1].Trim();
	}

	public static implicit operator IconifyIcon( string path ) => new IconifyIcon( path );

	public override string ToString()
	{
		return $"{Prefix}:{Name}";
	}
}