using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
namespace LANCommander.Launcher.Helpers;
///
/// Shared, RAM-only image cache for images loaded from either a local file path
/// or an http(s) URL. Decoded bitmaps are held in a byte-bounded LRU so images
/// that scroll back into view (or are revisited while navigating) render instantly
/// without re-fetching or re-decoding.
///
/// Bitmaps returned by / are owned by
/// the cache and must NOT be disposed by callers — eviction disposes them once they
/// fall out of the budget.
///
public static class RemoteImageCache
{
private static readonly HttpClient _httpClient = new();
private static readonly Dictionary _cache = new();
private static readonly LinkedList _order = new();
private static readonly Dictionary> _nodes = new();
private static readonly object _lock = new();
private static long _cacheBytes;
private const long MaxCacheBytes = 128L * 1024 * 1024;
///
/// Synchronous fast path: returns a cached bitmap for the given source/decode size
/// if one is present, refreshing its position in the LRU. Never touches disk or network.
///
public static bool TryGet(string source, int decodeWidth, int decodeHeight, out Bitmap? bitmap)
{
var key = BuildKey(source, decodeWidth, decodeHeight);
lock (_lock)
{
if (_cache.TryGetValue(key, out bitmap))
{
Touch(key);
return true;
}
}
bitmap = null;
return false;
}
///
/// Returns a decoded bitmap for (a local file path or an
/// http(s) URL), fetching and decoding off the calling thread when not already cached.
/// Pass to downscale by width, else
/// to downscale by height, else both zero to decode at full resolution.
///
public static async Task LoadAsync(string source, int decodeWidth, int decodeHeight, CancellationToken ct)
{
if (string.IsNullOrEmpty(source))
return null;
if (TryGet(source, decodeWidth, decodeHeight, out var cached))
return cached;
byte[]? data = null;
if (IsHttp(source))
{
data = await _httpClient.GetByteArrayAsync(source, ct);
if (ct.IsCancellationRequested)
return null;
}
var bitmap = await Task.Run(() => Decode(source, data, decodeWidth, decodeHeight), ct);
if (bitmap == null)
return null;
if (ct.IsCancellationRequested)
{
bitmap.Dispose();
return null;
}
return Insert(BuildKey(source, decodeWidth, decodeHeight), bitmap);
}
private static Bitmap? Decode(string source, byte[]? data, int decodeWidth, int decodeHeight)
{
Stream stream;
if (data != null)
{
stream = new MemoryStream(data);
}
else
{
if (!File.Exists(source))
return null;
stream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read);
}
using (stream)
{
if (decodeWidth > 0)
return Bitmap.DecodeToWidth(stream, decodeWidth, BitmapInterpolationMode.HighQuality);
if (decodeHeight > 0)
return Bitmap.DecodeToHeight(stream, decodeHeight, BitmapInterpolationMode.HighQuality);
return new Bitmap(stream);
}
}
///
/// Inserts a freshly-decoded bitmap, or returns the already-cached instance if another
/// caller decoded the same key concurrently (disposing the duplicate). Evicts
/// least-recently-used entries until back under the memory budget.
///
private static Bitmap Insert(string key, Bitmap bitmap)
{
lock (_lock)
{
if (_cache.TryGetValue(key, out var existing))
{
bitmap.Dispose();
Touch(key);
return existing;
}
_cache[key] = bitmap;
_nodes[key] = _order.AddLast(key);
_cacheBytes += BitmapBytes(bitmap);
// Evict LRU entries until under budget, always keeping the one just added.
while (_cacheBytes > MaxCacheBytes && _order.Count > 1)
{
var oldest = _order.First!.Value;
_order.RemoveFirst();
_nodes.Remove(oldest);
if (_cache.Remove(oldest, out var evicted))
{
_cacheBytes -= BitmapBytes(evicted);
evicted.Dispose();
}
}
return bitmap;
}
}
private static void Touch(string key)
{
if (_nodes.TryGetValue(key, out var node))
{
_order.Remove(node);
_order.AddLast(node);
}
}
private static string BuildKey(string source, int decodeWidth, int decodeHeight) =>
decodeWidth > 0 ? $"{source}|w{decodeWidth}"
: decodeHeight > 0 ? $"{source}|h{decodeHeight}"
: $"{source}|full";
private static bool IsHttp(string source) =>
source.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| source.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
private static long BitmapBytes(Bitmap bitmap) =>
(long)bitmap.PixelSize.Width * bitmap.PixelSize.Height * 4;
}