using Microsoft.AspNetCore.Components;
using Blazored.LocalStorage;
using Blog3000.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using Markdig.Extensions.Tables;
using Blog3000.Client.Services;
using System.Security.Cryptography;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace Blog3000.Client
{
///
/// TODO: Add all localStorage Ops regarding BlogsPosts here
/// - Updater
/// - Topic-Fetcher
/// - ...
///
public class BlogDb: IDisposable
{
public static string DBKEY_PREFIX_HDR = "blogdbhdr-";
public static string DBKEY_PREFIX_CONF = "blogdbconf-";
public static string DBKEY_PREFIX_BLOGCACHE = "blogcache-";
public static string DBKEY_PREFIX_IMGCACHE = "imgcached-";
public event EventHandler BlogPostsChanged;
private readonly ILocalStorageService localStorage;
private readonly ISyncLocalStorageService syncLocalStorage;
private readonly NetworkStatus networkStatus;
private readonly HttpClient httpClient;
private readonly ImgCacheStorage cache;
public BlogDb(ILocalStorageService ls, ISyncLocalStorageService sls, ImgCacheStorage cache, NetworkStatus ns, HttpClient http)
{
this.localStorage = ls;
this.syncLocalStorage = sls;
this.networkStatus = ns;
this.cache = cache;
this.httpClient = http;
localStorage.Changed += LocalStorage_Changed;
}
public void Dispose()
{
localStorage.Changed -= LocalStorage_Changed;
}
public enum FetchStatus
{
OK,
NotFound,
NetError,
Offline
}
private bool? _isEagerLoading = null;
public async Task IsEagerLoading()
{
if (_isEagerLoading != null) return _isEagerLoading == true;
_isEagerLoading = await localStorage.GetItemAsync($"{DBKEY_PREFIX_CONF}IsEagerLoading") == true;
return _isEagerLoading == true;
}
public void SetEagerLoading(bool enabled)
{
_isEagerLoading = enabled;
localStorage.SetItemAsync($"{DBKEY_PREFIX_CONF}IsEagerLoading", enabled);
}
public async Task AreImagesCached(string postId)
{
var imgsLoaded = await localStorage.GetItemAsync($"{DBKEY_PREFIX_IMGCACHE}{postId}");
if (imgsLoaded != null && true == imgsLoaded)
{
// Seems to have loaded once, check if still in cache
var bh = await GetHeaderAsync(postId);
if (bh == null) return false; // Should not occur but in case
if (bh.ImageRefs != null) {
var urls = new List();
foreach (var img in bh.ImageRefs)
{
// Images are cached by their id
var url = $"BlogPosts/{postId}/imgref/{img.Key}";
urls.Add(url);
}
var s = await cache.AreCached(urls.ToArray());
System.Diagnostics.Debug.WriteLine($"ImageCacheCheck: Found {s} for {postId}");
// Not in cache (anymore)?
if (!s) return false;
}
return true;
}
return false;
}
///
/// Get headers of all posts available (previously copied to localStorage)
///
///
public async IAsyncEnumerable GetHeadersAsync()
{
var res = await localStorage.GetItemAsync>(BlogDb.DBKEY_PREFIX_HDR + "remotePosts");
if (res != null)
{
foreach (var b in res)
{
yield return b;
}
}
}
///
/// Get a header of posts available (previously copied to localStorage)
///
///
///
public async Task GetHeaderAsync(string id)
{
if (id != null)
{
var res = await localStorage.GetItemAsync>(BlogDb.DBKEY_PREFIX_HDR + "remotePosts");
if (res != null)
{
foreach (var b in res)
{
if (id.Equals(b.Id)) return b;
}
}
}
return null;
}
///
/// Get headers from locally cached posts
///
///
public async IAsyncEnumerable GetHeadersFromCachedPostsAsync()
{
int n = await localStorage.LengthAsync();
for (int i = 0; i < n; i++)
{
var key = await localStorage.KeyAsync(i);
if (key != null && key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE))
{
var bpt = await localStorage.GetItemAsync(key);
if (bpt != null)
{
var bp = bpt.ToBlogPost();
yield return bp;
}
}
}
}
///
/// Get a locally cached post. Optionally fetch images from remote. Optionally fetch post from remote.
///
///
///
/// when true try loading uncached post, or in non-eager-loading mode refresh cached post, which has been changed TBD!!
///
public async Task<(BlogPost, FetchStatus)> GetCachedPostAsync(string postId, bool tryCacheImgs, bool tryRemote = false)
{
var res = await localStorage.GetItemAsync($"{DBKEY_PREFIX_BLOGCACHE}{postId}");
bool needsSync = false;
if (!(await IsEagerLoading()) && tryRemote && res != null && networkStatus.IsOnline /* makes no sense if offline */)
{
var bph = await GetHeaderAsync(postId);
if (bph != null)
{
if (! String.Equals(bph.Checksum, res.Checksum))
{
// Invalidate the result
System.Diagnostics.Debug.WriteLine("Post has been changed, and not refresh via auto-eagerloading, so forceing sync");
needsSync = true;
}
}
}
if (!needsSync)
{
//
// This check is only needed, when the post is cached, since syncing
// includeds the images anyway if tryCacheImgs==true
//
var imgsLoaded = await AreImagesCached(postId);
if (!imgsLoaded && tryCacheImgs)
{
if (networkStatus.IsOnline)
{
System.Diagnostics.Debug.WriteLine("Images not chached, forcing sync");
needsSync = true;
}
else
{
System.Diagnostics.Debug.WriteLine("Images not chached, but offline, so not forcing sync");
}
}
}
var status = FetchStatus.OK;
if (needsSync || res == null)
{
if (!tryRemote)
{
status = FetchStatus.NotFound;
}
else
{
var (sb, sres) = await SyncPost(postId, tryCacheImgs, false);
switch (sres)
{
case SyncStatus.OK: res = sb; break;
case SyncStatus.NetError: status = FetchStatus.NetError; break;
case SyncStatus.Offline: status = FetchStatus.Offline; break;
case SyncStatus.NotFound: status = FetchStatus.NotFound; break;
default:
throw new InvalidOperationException("unknown result");
}
if (status == FetchStatus.NotFound)
{
// When remote say not-found, ensure we won't handout cached data anymore
res = null;
}
else if (status != FetchStatus.OK && res != null)
{
// We can use the cached version, so return a Offline-Status event in Net-Error-case
status = FetchStatus.Offline;
}
}
}
return (res, status);
}
///
/// Get locally cached posts withText
///
///
public async IAsyncEnumerable GetCachedPostsAsync()
{
int n = await localStorage.LengthAsync();
for (int i = 0; i < n; i++)
{
var key = await localStorage.KeyAsync(i);
if (key != null && key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE))
{
var bpt = await localStorage.GetItemAsync(key);
yield return bpt;
}
}
}
public enum SyncStatus
{
OK,
Offline,
NotFound,
NetError
}
///
/// Sync a remote post with the local cache
///
///
///
///
///
public async Task<(BlogPost, SyncStatus)> SyncPost(string id, bool includeImgs, bool removeOld=true)
{
var status = SyncStatus.OK;
BlogPost res = null;
if (networkStatus.IsOnline)
{
try
{
if (removeOld)
{
//var oldPost = await localStorage.GetItemAsync($"{DBKEY_PREFIX}{id}");
//if (oldPost != null)
//{
// if (oldPost.ImageRefs != null)
// {
// foreach (var k in oldPost.ImageRefs.Keys)
// {
// await localStorage.RemoveItemAsync($"{DBKEY_IMG_PREFIX}{id}-{k}");
// }
// }
//}
var urls = await cache.MatchAll($"BlogPosts/{id}/imgref");
if (urls != null)
{
foreach (var u in urls)
{
await cache.Delete(u);
}
}
await Task.Yield();
await localStorage.RemoveItemAsync($"{DBKEY_PREFIX_IMGCACHE}{id}");
await localStorage.RemoveItemAsync($"{DBKEY_PREFIX_BLOGCACHE}{id}");
}
System.Diagnostics.Debug.WriteLine($"Syncing post {id}");
var newPost = await httpClient.GetJsonAsync($"BlogPosts/{id}");
await Task.Yield(); // Any effect after non-localhost-network-awaits?
if (newPost != null)
{
if (includeImgs)
{
if ((newPost.ImageRefs?.Count ?? 0) > 0)
{
//foreach (var k in newPost.ImageRefs.Keys)
//{
// try
// {
// System.Diagnostics.Debug.WriteLine($"Fetching {k} for {id}");
// var f = await httpClient.GetByteArrayAsync($"BlogPosts/{id}/imgref/{k}");
// await localStorage.SetItemAsync($"{BlogDb.DBKEY_IMG_PREFIX}{id}-{k}", f);
// }
// catch (HttpRequestException ex)
// {
// System.Diagnostics.Debug.WriteLine($"BlogPosts: error fetching img {ex.GetStatusCode()} from remote");
// }
//}
var urls = new List();
foreach (var k in newPost.ImageRefs.Keys)
{
var u = $"BlogPosts/{id}/imgref/{k}";
urls.Add(u);
}
System.Diagnostics.Debug.WriteLine($"BlogPosts: caching {urls.Count} imgs from remote... ");
await cache.AddAll(urls.ToArray());
await Task.Yield();
System.Diagnostics.Debug.WriteLine($".... done");
}
else
{
System.Diagnostics.Debug.WriteLine($"BlogPosts: no images to cache done");
}
await localStorage.SetItemAsync($"{BlogDb.DBKEY_PREFIX_IMGCACHE}{id}", true);
}
await localStorage.SetItemAsync(BlogDb.DBKEY_PREFIX_BLOGCACHE + id, newPost);
System.Diagnostics.Debug.WriteLine($"BlogPosts: fetched {id} from remote");
res= newPost;
}
else
{
status = SyncStatus.NotFound;
}
}
catch (HttpRequestException ex)
{
System.Diagnostics.Debug.WriteLine($"BlogPosts: error fetching {ex.GetStatusCode()} from remote");
// TODO: Find better way to extract statuscode
if (("" + ex.ToString()).Contains("404")) status = SyncStatus.NotFound;
else status = SyncStatus.NetError;
}
}
else
{
status = SyncStatus.Offline;
}
return (res, status);
}
private void LocalStorage_Changed(object sender, ChangedEventArgs e)
{
// Notify if a cached posts were changed
if (e.Key.StartsWith(BlogDb.DBKEY_PREFIX_BLOGCACHE)) Task.Run(() => {
System.Diagnostics.Debug.WriteLine("LocalStorage CachedPosts updated event -> emitting blogDbChanged Event");
BlogPostsChanged?.Invoke(this, EventArgs.Empty);
});
// Notify if header-list was changed
if (e.Key.StartsWith(BlogDb.DBKEY_PREFIX_HDR)) Task.Run(() =>
{
System.Diagnostics.Debug.WriteLine("LocalStorage Headers updated event -> emitting blogDbChanged Event");
BlogPostsChanged?.Invoke(this, EventArgs.Empty);
});
}
}
}