PixelRef.cs
namespace PixelPusher;

/// <summary>
/// Temporary tracing overlay. Never written to saves, drafts, or the Library.
/// </summary>
public static class PixelRef
{
	static readonly string[] Hosts =
	{
		"files.catbox.moe",
		"litterbox.catbox.moe",
		"files.litterbox.catbox.moe",
		"i.imgur.com",
		"imgur.com",
		"i.ibb.co",
		"ibb.co",
		"i.postimg.cc",
		"postimg.cc",
		"raw.githubusercontent.com",
		"github.com",
		"i.gyazo.com",
		"media.discordapp.net",
		"cdn.discordapp.com",
		"i.redd.it",
		"preview.redd.it",
		"pomf.lain.la",
		"0x0.st",
		"tmpfiles.org"
	};

	public static bool Visible { get; private set; }
	public static bool Fit { get; set; } = true;
	public static int Opacity { get; private set; } = 40;
	public static int Version { get; private set; }
	public static string Status { get; private set; } = "no trace";
	public static bool Busy { get; private set; }

	static Color32[] _px;
	static int _w;
	static int _h;

	public static bool HasImage => _px is { Length: > 0 } && _w > 0 && _h > 0;

	public static void Drop()
	{
		_px = null;
		_w = 0;
		_h = 0;
		Visible = false;
		Status = "no trace";
		Version++;
		GameFlow.ShowToast( "trace dropped" );
		GameFlow.Bump();
	}

	public static void Toggle()
	{
		if ( !HasImage )
			return;
		Visible = !Visible;
		Version++;
		GameFlow.Bump();
	}

	public static void ToggleFit()
	{
		Fit = !Fit;
		Version++;
		GameFlow.Bump();
	}

	public static void NudgeOpacity( int dir )
	{
		Opacity = Math.Clamp( Opacity + dir, 10, 80 );
		Version++;
		GameFlow.Bump();
	}

	public static void Paste()
	{
		var url = ReadClipboard();
		Load( url );
	}

	public static async void Load( string url )
	{
		url = (url ?? "").Trim().Trim( '"', '\'', '<', '>' );
		if ( url.Length < 12 )
		{
			GameFlow.ShowToast( "copy an image URL first" );
			return;
		}

		if ( !Uri.TryCreate( url, UriKind.Absolute, out var uri ) )
		{
			GameFlow.ShowToast( "not a URL" );
			return;
		}

		if ( !HostOk( uri.Host ) || !UrlAllowed( uri ) )
		{
			GameFlow.ShowToast( "host not allowed" );
			return;
		}

		if ( LooksLikePage( uri ) )
		{
			GameFlow.ShowToast( "need a direct image link" );
			return;
		}

		Busy = true;
		Status = "loading";
		Version++;
		GameFlow.Bump();

		try
		{
			var tex = await Texture.LoadAsync( uri.ToString() );
			if ( !tex.IsValid() )
				throw new Exception( "bad image" );

			var w = tex.Width;
			var h = tex.Height;
			if ( w < 2 || h < 2 )
				throw new Exception( "too small" );
			if ( w > 2048 || h > 2048 )
				throw new Exception( "too large" );

			var px = ReadPixels( tex, w, h );
			if ( px is null || px.Length < w * h )
				throw new Exception( "could not read pixels" );

			_px = px;
			_w = w;
			_h = h;
			Opacity = 40;
			Visible = true;
			Status = $"{w}x{h} trace";
			Version++;
			GameFlow.ShowToast( "trace on  ·  not saved" );
		}
		catch ( Exception e )
		{
			Status = "failed";
			GameFlow.ShowToast( $"trace failed  {e.Message}" );
			Log.Warning( $"[PIXEL PUSHER] trace: {e.Message}" );
		}

		Busy = false;
		Version++;
		GameFlow.Bump();
	}

	public static Color32 Sample( int cx, int cy, int canvasW, int canvasH )
	{
		if ( !Visible || !HasImage || canvasW <= 0 || canvasH <= 0 )
			return default;
		if ( (uint)cx >= (uint)canvasW || (uint)cy >= (uint)canvasH )
			return default;

		int rx;
		int ry;
		if ( Fit )
		{
			rx = cx * _w / canvasW;
			ry = cy * _h / canvasH;
		}
		else
		{
			rx = cx;
			ry = cy;
		}

		if ( (uint)rx >= (uint)_w || (uint)ry >= (uint)_h )
			return default;

		var src = _px[ry * _w + rx];
		Color c = src;
		if ( c.a < 0.04f )
			return default;
		var a = (byte)Math.Clamp( Opacity * 255 / 100, 0, 200 );
		return new Color32(
			(byte)MathF.Round( c.r * 255f ),
			(byte)MathF.Round( c.g * 255f ),
			(byte)MathF.Round( c.b * 255f ),
			a );
	}

	static bool HostOk( string host )
	{
		host = (host ?? "").ToLowerInvariant();
		if ( host.StartsWith( "www." ) )
			host = host[4..];
		foreach ( var h in Hosts )
		{
			if ( host == h || host.EndsWith( "." + h ) )
				return true;
		}
		return false;
	}

	static bool LooksLikePage( Uri uri )
	{
		var path = uri.AbsolutePath ?? "";
		if ( path.EndsWith( ".png", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( path.EndsWith( ".jpg", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( path.EndsWith( ".jpeg", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( path.EndsWith( ".gif", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( path.EndsWith( ".webp", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( path.EndsWith( ".bmp", StringComparison.OrdinalIgnoreCase ) ) return false;
		if ( hostIsCatboxFile( uri ) ) return false;
		return path.Length < 2 || path.EndsWith( "/" ) || !path.Contains( '.' );
	}

	static bool hostIsCatboxFile( Uri uri )
	{
		var host = uri.Host.ToLowerInvariant();
		return host.Contains( "catbox.moe" ) && uri.AbsolutePath.Length > 2;
	}

	static bool UrlAllowed( Uri uri )
	{
		try
		{
			return Http.IsAllowed( uri );
		}
		catch
		{
			return uri.Scheme == "https" || uri.Scheme == "http";
		}
	}

	static string ReadClipboard()
	{
		return "";
	}

	static Color32[] ReadPixels( Texture tex, int w, int h )
	{
		try
		{
			var data = tex.GetPixels();
			if ( data is { Length: > 0 } )
				return data;
		}
		catch
		{
			// try per-pixel
		}

		var px = new Color32[w * h];
		try
		{
			for ( var y = 0; y < h; y++ )
			for ( var x = 0; x < w; x++ )
				px[y * w + x] = tex.GetPixel( x, y );
			return px;
		}
		catch
		{
			return null;
		}
	}
}