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

using System.IO.Compression;

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

using Plugin.Firebase.Firestore;
using Plugin.Firebase.Storage;

using MonkeyCache.FileStore;

using DXLib.UI.Alert;
using DXLib.Log;

namespace DXLib.Data
{
	/*
	 * Provides utility methods for working with the Firestore database, Firebase Cloud Storage, and MonkeyBarrel cache.
	 */
	public static partial class DXData
	{
		/* Constants */

		// Max writes per batch update
		public const int MaxBatchSize = 500;

		// Used for numeric NaN data formatting
		public const string NaN = "--";

		// Read/Write buffer (64K)
		private const int BufferSize = (64 * 1024);

		// Object writing content type
		private const string ContentType = "application/octet-stream";

		// Cache duration (1 year)
		private const int CacheDuration = (365 * 1);

		/* Properties */

		// Static accessors
		public static IFirebaseFirestore Firestore => CrossFirebaseFirestore.Current;
		public static IFirebaseStorage Storage => CrossFirebaseStorage.Current;

		/* Methods */

		// Initializes Firestore and cache settings at app launch
		public static void Init( string appId )
		{
			string host = Firestore.Settings.Host;
			long cache = Firestore.Settings.CacheSizeBytes;
			
			// Make sure persistence enabled
			Firestore.Settings = new FirestoreSettings( host, isPersistenceEnabled:true, isSslEnabled:true, cache );

			// Init MonkeyBarrel
			Barrel.ApplicationId = appId;
		}

		// Determines if app currently has active Internet connection
		public static bool HasConnection()
		{
			return (Connectivity.NetworkAccess == NetworkAccess.Internet);
		}

		// Generates globally unique identifier for database documents
		public static string Guid()
		{
			return System.Guid.NewGuid().ToString();
		}

		// Determines if specified URI is valid format
		public static bool IsValidUri( string uri )
		{
			// Basic internal test
			bool uriValid = Uri.IsWellFormedUriString( uri, UriKind.Absolute );

			Regex regex = UriRegex();
			
			// RegEx test
			bool regexValid = regex.IsMatch( uri! );

			// Both must be valid
			return uriValid && regexValid;
		}

		/* Error */

		// Handles data error for cloud read/write
		public static void HandleError( string key, Exception ex, bool alert = true )
		{
			// Persist to log
			DXLog.Exception( key, ex );

			// Optionally show to user
			if ( alert )
			{
				DXAlert.ShowError( "net.err", "net.err.update" );
			}
		}

		/* Database */

		// Shorthand for starting Firestore multi-write batch
		public static IWriteBatch StartBatch()
		{
			return Firestore.CreateBatch();
		}

		// Persists specified batch
		public static async Task CommitBatch( IWriteBatch batch )
		{
            await batch.CommitAsync();
        }

		// Builds list of concrete objects from specified query data
		public static List<T> BuildList<T>( IQuerySnapshot<T> snapshot )
		{
			List<T> list = [];
			
			// Build list
			foreach ( IDocumentSnapshot<T> document in snapshot.Documents )
			{
				try
				{ 
					list.Add( document.Data );
				}
				catch ( Exception ex )
				{
					DXLog.Exception( "data.list", ex );
				}
			}

			return list;
		}
		
        /* Storage */

        // URIs

        // Creates URL for remote Firebase Storage file
        public static string CreateUrl( string path, string file, string ext )
		{
			return $"{path.ToLower()}/{file}.{ext}";
		}

		// Returns URI for loading data from specified Firebase Storage file
		public static async Task<string> GetDataUri( string path )
		{
			return await GetReference( path ).GetDownloadUrlAsync();
		}

		// Returns reference to remote Firebase Cloud Storage file at URL
		private static IStorageReference GetReference( string url )
		{
			return Storage.GetRootReference().GetChild( url );
		}

		// Read

		// Reads data from remote Firebase Cloud Storage at specified URL
		public static async Task<byte[]> ReadStorage( string url, long maxBytes, bool alert = true )
		{
			try
			{
				return await GetReference( url ).GetBytesAsync( maxBytes );
			}
			catch ( Exception ex )
			{
				HandleError( $"storage.read.{url}", ex, alert );

				return null;
			}
		}

		// Reads object from Firebase Cloud Storage at specified URL
		public static async Task<T> ReadStorage<T>( string url, bool alert = true )
		{
			try
			{
				// Read raw bytes from cloud
				byte[] bytesIn = await ReadStorage( url, (100 * 1024), alert );

				if ( bytesIn == null )
				{
					return default;
				}

				// Gzip decompress
				byte[] bytesOut = Decompress( bytesIn );

				// Deserialize JSON
				string json = Encoding.Unicode.GetString( bytesOut );

				// Convert to C# object
				return JsonSerializer.Deserialize<T>( json );
			}
			catch ( Exception ex )
			{
				HandleError( $"storage.read2.{url}", ex, alert );
			}

			return default;
		}

		// Write

		// Writes specified object to remote Firebase Cloud Storage at URL
		public static async Task WriteStorage( string url, object obj, Action callback = null )
		{
			// Serialize to JSON
			string json = JsonSerializer.Serialize( obj );

			// Get raw bytes
			byte[] bytesIn = Encoding.Unicode.GetBytes( json );

			// Gzip compress
			byte[] bytesOut = Compress( bytesIn );

			// Write to cloud
			await WriteStorage( url, bytesOut, ContentType, callback );
		}

		// Writes specified raw data to remote Firebase Cloud Storage at URL
		public static async Task WriteStorage( string url, byte[] bytes, string contentType, Action callback = null )
		{
			// Must set content type for images
			StorageMetadata metadata = new( contentType: contentType );

			// Setup
			IStorageTransferTask task = GetReference( url ).PutBytes( bytes, metadata );

			// Register listener
			if ( callback != null )
			{
				task.AddObserver( StorageTaskStatus.Success, ( s ) =>
				{
					callback.Invoke();
				});
			}

			// Write
			await task.AwaitAsync();
		}

		// Delete

		// Deletes specified file from remote Firebase Cloud Storage
		public static async Task DeleteStorage( string url )
		{
			try
			{
				IStorageReference reference = GetReference( url );
				
				await reference.DeleteAsync();
			}
			// Exception will be thrown if file does not exist (non-fatal)
			catch ( Exception ex )
			{
				DXLog.Exception( $"storage.delete.{url}", ex );
			}
		}

		/* Cache */

		// Returns list of all active MonkeyBarrel cache keys
		public static IEnumerable<string> GetCacheKeys()
		{
			return Barrel.Current.GetKeys( state:MonkeyCache.CacheState.Active );
		}

		// Reads object matching specified key from local MonkeyBarrel cache
		public static T ReadCache<T>( string key )
		{
			return Barrel.Current.Get<T>( key );
		}

		// Writes specified object to local MonkeyBarrel cache
		public static void WriteCache<T>( string key, T obj, bool replace = true )
		{
			if ( replace && Barrel.Current.Exists( key ) )
			{
				DeleteCache( key );
			}

			Barrel.Current.Add( key, obj, TimeSpan.FromDays( CacheDuration ) );
		}

		// Deletes specified object from local cache
		public static void DeleteCache( string key )
		{
			Barrel.Current.Empty( key );
		}

		/* URL File */

		// Reads and returns all bytes from file at specified path
		public static byte[] ReadFromFile( string path )
		{
			// Open stream
			FileStream stream = new( path, FileMode.Open, FileAccess.Read );

			try
			{
				int count;
				int sum = 0;

				// Allocate buffer
				int len = (int)stream.Length;
				byte[] buffer = new byte[ len ];

				// Read until EOF
				while ( (count = stream.Read( buffer, sum, (len - sum) )) > 0 )
				{
					sum += count;
				}

				return buffer;
			}
			finally
			{
				stream.Close();
			}
		}

		/* GZip */

		// Gzip compresses specified input string
		public static byte[] Compress( string input )
		{
			byte[] bytesIn = Encoding.UTF8.GetBytes( input );

			return Compress( bytesIn );
		}

		// Gzip compresses specified input bytes
		public static byte[] Compress( byte[] bytesIn )
		{
			using MemoryStream stream = new();

			// Gzip compress
			using ( BufferedStream gzip = new( new GZipStream( stream, CompressionMode.Compress ), BufferSize ) )
			{
				gzip.Write( bytesIn, 0, bytesIn.Length );
			}

			return stream.ToArray();
		}

		// Gzip decompresses specified input bytes
		public static byte[] Decompress( byte[] bytesIn )
		{
			using MemoryStream stream = new( bytesIn );
			using MemoryStream decompressed = new();

			// Decompress gzip
			using ( BufferedStream gzip = new( new GZipStream( stream, CompressionMode.Decompress ), BufferSize ) )
			{
				gzip.CopyTo( decompressed );
			}

			return decompressed.ToArray();
		}

		/* Binary */

		// Extracts a 16bit short from byte array at specified offset
		public static short ShortFromBytes( byte[] bytes, int offset )
		{
			try
			{
				return BitConverter.ToInt16( bytes, offset );
			}
			catch ( Exception )
			{
				return short.MaxValue;
			}
		}

		/* Regex */
		
		// Validates URI format
        [GeneratedRegex(@"(http|https)://[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?")]
        private static partial Regex UriRegex();
    }
}

//
