using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using XenseAR;
public class AssetBundleLoader
{

    MonoBehaviour masterMonoBehaviour;
    string bundleURL = "";
    private string bundleName = "";
    UnityWebRequest www;

    public AssetBundleLoader(MonoBehaviour monoBehaviour, string bundleUrl, string bundleName)
    {
        masterMonoBehaviour = monoBehaviour;
        this.bundleURL = bundleUrl;
        this.bundleName = bundleName;
    }

    public AssetBundleLoader(MonoBehaviour monoBehaviour)
    {
        masterMonoBehaviour = monoBehaviour;
    }

    public async UniTask<bool> LoadAsync(Action<GameObject> instantiateObject = null, bool instantiate = true, UnityAction callback = null, CancellationToken token = default)
    {
        try
        {
            return await DownloadAndCacheAsync(instantiateObject, instantiate, callback, token);
        }
        catch (Exception e)
        {
            Debug.Log("Exception " + e);
            return false;
        }
    }

    async UniTask<bool> DownloadAndCacheAsync(Action<GameObject> instantiateObject = null, bool instantiate = true,
      UnityAction callback = null, CancellationToken token = default)
    {
#if UNITY_ANDROID
        string bundleURL_post = bundleURL + Defines.androidPostfix;
#elif UNITY_IOS
        string bundleURL_post = bundleURL + Defines.iosPostfix;
#elif UNITY_WEBGL
        string bundleURL_post = bundleURL + Defines.webglPostfix;
#elif UNITY_WSA
        string bundleURL_post = bundleURL + Defines.wsaPlayerPostfix;
#elif UNITY_STANDALONE_WIN
#if UNITY_SERVER
        string bundleURL_post = bundleURL + Defines.standaloneWindows64Postfix + Defines.serverPostfix;
#else
        string bundleURL_post = bundleURL + Defines.standaloneWindows64Postfix;
#endif
#elif UNITY_STANDALONE_OSX
#if UNITY_SERVER
        string bundleURL_post = bundleURL + Defines.standaloneOSXPostfix + Defines.serverPostfix;
#else
        string bundleURL_post = bundleURL + Defines.standaloneOSXPostfix;
#endif
#elif UNITY_STANDALONE_LINUX
#if UNITY_SERVER
        string bundleURL_post = bundleURL + Defines.standaloneLinuxPostfix + Defines.serverPostfix;
#else
        string bundleURL_post = bundleURL + Defines.standaloneLinuxPostfix;
#endif
#else
        string bundleURL_post = "";
#endif
        string bundlePath = bundleURL_post;
        string CacheFile = "";
#if !UNITY_WEBGL
        CacheFile = GetCacheFile(bundlePath);
        (bool useLocal, Hash128? hash128) = await VerifyLocalFileVersion(CacheFile, bundlePath);

        if (!useLocal && hash128 != null)
        {
            bundlePath = bundleURL_post + "?r=" + (UnityEngine.Random.value * 9999999);
            www = UnityWebRequestAssetBundle.GetAssetBundle(bundlePath, hash128.Value, 0);
        }
        else if (!useLocal)
        {
            www = UnityWebRequest.Get(bundleURL_post);
        }
#else
        www = UnityWebRequestAssetBundle.GetAssetBundle(bundleURL_post);
        bool useLocal = false;
        Hash128? hash128 = null;
#endif

#if UNITY_EDITOR
        www.timeout = 0;
#endif
        AssetBundle bundle = null;
        if (!useLocal)
        {
            await www.SendWebRequest().WithCancellation(token);
            if (www.result != UnityWebRequest.Result.Success)
            {
                //Result of DataProcessingError will not result in www.error, but still fail to use
                Debug.LogError($"Asset Bundle Request fail with result:{www.result}. Error: {www.error}");
                www.Dispose();
                www = null;
                return false;
            }
            //AssetBundle myLoadedAssetBundle = AssetBundle.GetAllLoadedAssetBundles().FirstOrDefault(bundle => bundle.name == bundlePath);
            IEnumerable<AssetBundle> myLoadedAssetBundle = AssetBundle.GetAllLoadedAssetBundles();
            while (myLoadedAssetBundle.Where(bundle => bundle.name == bundlePath).Count() > 0)
            {
                await UniTask.Yield();
            }
            if (hash128 == null)
            {
                byte[] bytes = www.downloadHandler.data;
                bundle = await AssetBundle.LoadFromMemoryAsync(bytes);
                SaveToCache(CacheFile, bytes);
            }
            else
            {
                bundle = DownloadHandlerAssetBundle.GetContent(www);
            }                
        }
        else
        {
            bundle = await AssetBundle.LoadFromFileAsync(CacheFile);
        }

        if (bundle == null)
        {
            Debug.LogError($"Fail to get content with bundle: {www.url}");
            www.Dispose();
            www = null;
            return false;
        }

        GameObject bundlePrefab = null;
        if (instantiate)
        {
            if (bundleName == "")
            {
                bundleName = bundle.GetAllAssetNames()[0];
            }
            AssetBundleRequest request = bundle.LoadAssetAsync<GameObject>(bundleName);
            await request.ToUniTask();
            bundlePrefab = request.asset as GameObject;

            if (bundlePrefab != null)
            {
                var go = GameObject.Instantiate(bundlePrefab, masterMonoBehaviour.transform);
                ReApplyShaders.Start(go);
                if (instantiateObject != null) instantiateObject.Invoke(go);
            }
        }

        www.Dispose();
        www = null;

        await Resources.UnloadUnusedAssets();
        await bundle.UnloadAsync(false);
        bundle = null;
        callback?.Invoke();
        return true;
    }

    /// <summary>
    /// Verify if the local file exist, if it is the same version as remote file
    /// </summary>
    /// <param name="localABPath"></param>
    /// <param name="remoteABPath"></param>
    /// <returns></returns>
    public async UniTask<(bool, Hash128?)> VerifyLocalFileVersion(string localABPath, string remoteABPath)
    {
        if (!File.Exists(localABPath)) return (false, null);

        // Cache date
        var cacheInfo = new FileInfo(localABPath);
        var cacheSize = cacheInfo.Length;
        var cacheDate = cacheInfo.LastWriteTimeUtc;
        DateTimeOffset cacheTime = new DateTimeOffset(cacheDate, TimeSpan.Zero);
        // Request date
        www = UnityWebRequest.Head(remoteABPath);
        www.timeout = 5;

        try
        {
            await www.SendWebRequest();

            if (www.result == UnityWebRequest.Result.Success)
            {
                string lastModifiedHeader = www.GetResponseHeader("Last-Modified");
                string contentLengthHeader = www.GetResponseHeader("Content-Length");

                bool dateChanged = false;
                bool sizeChanged = false;

                if (!string.IsNullOrEmpty(lastModifiedHeader) && DateTimeOffset.TryParse(
                        lastModifiedHeader,
                        CultureInfo.InvariantCulture,
                        DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
                        out DateTimeOffset remoteTime))
                {
                    dateChanged = cacheTime.UtcDateTime < remoteTime.UtcDateTime - TimeSpan.FromMinutes(10);
                }

                if (!string.IsNullOrEmpty(contentLengthHeader) && long.TryParse(contentLengthHeader, out long remoteSize))
                {
                    sizeChanged = remoteSize != cacheSize;
                }

                www.Dispose();

                if (dateChanged || sizeChanged) return (false, null);
            }
            else
            {
                Debug.LogError($"Unable to check remote link {remoteABPath}; Error: {www.error}");
                www.Dispose();
                return (true, null);
            }

            try
            {
                www = UnityWebRequest.Get(remoteABPath + ".manifest?r=" + (UnityEngine.Random.value * 9999999));
                await www.SendWebRequest();

                if (www.result == UnityWebRequest.Result.Success)
                {
                    Hash128 hashString = default(Hash128);

                    if (www.downloadHandler.text.Contains("ManifestFileVersion"))
                    {
                        var hashes = www.downloadHandler.text
                                    // separate the text into lines, in an list/array of strings
                                    .Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries)
                                    // select only the lines that starts with 'lookFor', ignoring white spaces
                                    .Where(x => x.Trim().StartsWith("Hash:"))
                                    // convert result to array
                                    .ToArray();
                        string myParsedHash = hashes
                            // on the manifest file, the hash we need is always the first (AssetFileHash), so here we just take the first one found.
                            .FirstOrDefault()
                            // ignore whitespaces
                            .Trim();
                        myParsedHash = string.IsNullOrEmpty(myParsedHash) ? "" :
                            // get the right part of the string after what 'lookfor'. +1 because the substring index starts at zero
                            myParsedHash.Substring("Hash:".Length + 1);

                        hashString = Hash128.Parse(myParsedHash.Trim());
                        www.Dispose();
                        if (hashString.isValid)
                        {
                            return (false, hashString);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.LogError($"Unable to check remote manifest {remoteABPath + ".manifest"}; Error: {www.error}");
            }
        }
        catch (Exception ex)
        {
            Debug.LogError($"Unable to check remote link {remoteABPath}; Error: {www.error}");
        }
        return (true, null);
    }

    public float getDownloadProgress()
    {
        if (www != null)
        {
            return www.downloadProgress;
        }
        return 0;
    }

    protected void SaveToCache(string CacheFile, byte[] data)
    {
        if (File.Exists(CacheFile))
        {
            File.Delete(CacheFile);
        }
        File.WriteAllBytes(CacheFile, data);
    }

    public string GetCacheFile(string url)
    {
        string CachePath = Application.persistentDataPath;

        CachePath = Path.Combine(CachePath, "AnchorCaches");
        if (!Directory.Exists(CachePath))
        {
            Directory.CreateDirectory(CachePath);
        }

        Uri uri = new Uri(url);
        string[] folderNames = uri.AbsolutePath.Split("/", StringSplitOptions.RemoveEmptyEntries);
        if (folderNames.Length == 0)
        {
            Debug.Log("Something wrong with the storage!!!");
            return CachePath;
        }
        else
        {
            for (int i = 0; i < folderNames.Length - 1; i++)
            {
                CachePath = Path.Combine(CachePath, folderNames[i]);
                if (!Directory.Exists(CachePath))
                {
                    Directory.CreateDirectory(CachePath);
                }
            }
        }
        string fileName = Path.GetFileName(url);
        string CacheFile = Path.Combine(CachePath, fileName);
        return CacheFile;
    }
}
