using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.ResourceManagement.Util;

namespace UnityEngine.AddressableAssets.ResourceProviders
{
    /// <summary>
    /// Provider for content catalogs.  This provider makes use of a hash file to determine if a newer version of the catalog needs to be downloaded.
    /// </summary>
    [DisplayName("Content Catalog Provider")]
    public class ContentCatalogProvider : ResourceProviderBase
    {
        /// <summary>
        /// Options for specifying which entry in the catalog dependencies should hold each hash item.
        ///  The Remote should point to the hash on the server.  The Cache should point to the
        ///  local cache copy of the remote data.
        /// </summary>
        public enum DependencyHashIndex
        {
            /// <summary>
            /// Use to represent the index of the remote entry in the dependencies list.
            /// </summary>
            Remote = 0,

            /// <summary>
            /// Use to represent the index of the cache entry in the dependencies list.
            /// </summary>
            Cache,

            /// <summary>
            /// Use to represent the number of entries in the dependencies list.
            /// </summary>
            Count
        }

        /// <summary>
        /// Use to indicate if the updating the catalog on startup should be disabled.
        /// </summary>
        public bool DisableCatalogUpdateOnStart = false;

        /// <summary>
        /// Use to indicate if the local catalog is in a bundle.
        /// </summary>
        public bool IsLocalCatalogInBundle = false;

        internal Dictionary<IResourceLocation, InternalOp> m_LocationToCatalogLoadOpMap = new Dictionary<IResourceLocation, InternalOp>();
        ResourceManager m_RM;

        /// <summary>
        /// Constructor for this provider.
        /// </summary>
        /// <param name="resourceManagerInstance">The resource manager to use.</param>
        public ContentCatalogProvider(ResourceManager resourceManagerInstance)
        {
            m_RM = resourceManagerInstance;
            m_BehaviourFlags = ProviderBehaviourFlags.CanProvideWithFailedDependencies;
        }

        /// <inheritdoc/>
        public override void Release(IResourceLocation location, object obj)
        {
            if (m_LocationToCatalogLoadOpMap.ContainsKey(location))
            {
                m_LocationToCatalogLoadOpMap[location].Release();
                m_LocationToCatalogLoadOpMap.Remove(location);
            }

            base.Release(location, obj);
        }

        internal class InternalOp
        {
            //   int m_StartFrame;
            string m_LocalDataPath;
            string m_RemoteHashValue;
            internal string m_LocalHashValue;
            ProvideHandle m_ProviderInterface;
            internal ContentCatalogData m_ContentCatalogData;
            AsyncOperationHandle<ContentCatalogData> m_ContentCatalogDataLoadOp;
            private BundledCatalog m_BundledCatalog;
            private bool m_Retried;
            private bool m_DisableCatalogUpdateOnStart;
            private bool m_IsLocalCatalogInBundle;

            public void Start(ProvideHandle providerInterface, bool disableCatalogUpdateOnStart, bool isLocalCatalogInBundle)
            {
                m_ProviderInterface = providerInterface;
                m_DisableCatalogUpdateOnStart = disableCatalogUpdateOnStart;
                m_IsLocalCatalogInBundle = isLocalCatalogInBundle;
                m_ProviderInterface.SetWaitForCompletionCallback(WaitForCompletionCallback);
                m_LocalDataPath = null;
                m_RemoteHashValue = null;

                List<object> deps = new List<object>(); // TODO: garbage. need to pass actual count and reuse the list
                m_ProviderInterface.GetDependencies(deps);
                string idToLoad = DetermineIdToLoad(m_ProviderInterface.Location, deps, disableCatalogUpdateOnStart);

                Addressables.LogFormat("Addressables - Using content catalog from {0}.", idToLoad);

                bool loadCatalogFromLocalBundle = isLocalCatalogInBundle && CanLoadCatalogFromBundle(idToLoad, m_ProviderInterface.Location);

                LoadCatalog(idToLoad, loadCatalogFromLocalBundle);
            }

            bool WaitForCompletionCallback()
            {
                if (m_ContentCatalogData != null)
                    return true;
                bool ccComplete;
                if (m_BundledCatalog != null)
                {
                    ccComplete = m_BundledCatalog.WaitForCompletion();
                }
                else
                {
                    ccComplete = m_ContentCatalogDataLoadOp.IsDone;
                    if (!ccComplete)
                        m_ContentCatalogDataLoadOp.WaitForCompletion();
                }

                //content catalog op needs the Update to be pumped so we can invoke completion callbacks
                if (ccComplete && m_ContentCatalogData == null)
                    m_ProviderInterface.ResourceManager.Update(Time.unscaledDeltaTime);

                return ccComplete;
            }

            /// <summary>
            /// Clear all content catalog data.
            /// </summary>
            public void Release()
            {
                m_ContentCatalogData?.CleanData();
            }

            internal bool CanLoadCatalogFromBundle(string idToLoad, IResourceLocation location)
            {
                return Path.GetExtension(idToLoad) == ".bundle" &&
                       idToLoad.Equals(GetTransformedInternalId(location));
            }

            internal void LoadCatalog(string idToLoad, bool loadCatalogFromLocalBundle)
            {
                try
                {
                    ProviderLoadRequestOptions providerLoadRequestOptions = null;
                    if (m_ProviderInterface.Location.Data is ProviderLoadRequestOptions providerData)
                        providerLoadRequestOptions = providerData.Copy();

                    if (loadCatalogFromLocalBundle)
                    {
                        int webRequestTimeout = providerLoadRequestOptions?.WebRequestTimeout ?? 0;
                        m_BundledCatalog = new BundledCatalog(idToLoad, webRequestTimeout);
                        m_BundledCatalog.OnLoaded += ccd =>
                        {
                            m_ContentCatalogData = ccd;
                            OnCatalogLoaded(ccd);
                        };
                        m_BundledCatalog.LoadCatalogFromBundleAsync();
                    }
                    else
                    {
#if ENABLE_BINARY_CATALOG
                        ResourceLocationBase location = new ResourceLocationBase(idToLoad, idToLoad,
                            typeof(BinaryAssetProvider<ContentCatalogData.Serializer>).FullName, typeof(ContentCatalogData));
                        location.Data = providerLoadRequestOptions;
                        m_ProviderInterface.ResourceManager.ResourceProviders.Add(new BinaryAssetProvider<ContentCatalogData.Serializer>());
#else
                        ResourceLocationBase location = new ResourceLocationBase(idToLoad, idToLoad,
                           typeof(JsonAssetProvider).FullName, typeof(ContentCatalogData));
#endif
                        m_ContentCatalogDataLoadOp = m_ProviderInterface.ResourceManager.ProvideResource<ContentCatalogData>(location);
                        m_ContentCatalogDataLoadOp.Completed += CatalogLoadOpCompleteCallback;
                    }
                }
                catch (Exception ex)
                {
                    m_ProviderInterface.Complete<ContentCatalogData>(null, false, ex);
                }
            }

            void CatalogLoadOpCompleteCallback(AsyncOperationHandle<ContentCatalogData> op)
            {
                m_ContentCatalogData = op.Result;
                m_ProviderInterface.ResourceManager.Release(op);
                OnCatalogLoaded(m_ContentCatalogData);
            }

            internal class BundledCatalog
            {
                private readonly string m_BundlePath;
                private bool m_OpInProgress;
                private AssetBundleCreateRequest m_LoadBundleRequest;
                internal AssetBundle m_CatalogAssetBundle;
                private AssetBundleRequest m_LoadTextAssetRequest;
                private ContentCatalogData m_CatalogData;
                private WebRequestQueueOperation m_WebRequestQueueOperation;
                private AsyncOperation m_RequestOperation;
                private int m_WebRequestTimeout;

                public event Action<ContentCatalogData> OnLoaded;

                public bool OpInProgress => m_OpInProgress;
                public bool OpIsSuccess => !m_OpInProgress && m_CatalogData != null;

                public BundledCatalog(string bundlePath, int webRequestTimeout = 0)
                {
                    if (string.IsNullOrEmpty(bundlePath))
                    {
                        throw new ArgumentNullException(nameof(bundlePath), "Catalog bundle path is null.");
                    }
                    else if (!bundlePath.EndsWith(".bundle", StringComparison.OrdinalIgnoreCase))
                    {
                        throw new ArgumentException("You must supply a valid bundle file path.");
                    }

                    m_BundlePath = bundlePath;
                    m_WebRequestTimeout = webRequestTimeout;
                }

                ~BundledCatalog()
                {
                    Unload();
                }

                private void Unload()
                {
                    m_CatalogAssetBundle?.Unload(true);
                    m_CatalogAssetBundle = null;
                }

                public void LoadCatalogFromBundleAsync()
                {
                    //Debug.Log($"LoadCatalogFromBundleAsync frame : {Time.frameCount}");
                    if (m_OpInProgress)
                    {
                        Addressables.LogError($"Operation in progress : A catalog is already being loaded. Please wait for the operation to complete.");
                        return;
                    }

                    m_OpInProgress = true;

                    if (ResourceManagerConfig.ShouldPathUseWebRequest(m_BundlePath))
                    {
                        var req = UnityWebRequestAssetBundle.GetAssetBundle(m_BundlePath);
                        if (m_WebRequestTimeout > 0)
                            req.timeout = m_WebRequestTimeout;

                        m_WebRequestQueueOperation = WebRequestQueue.QueueRequest(req);
                        if (m_WebRequestQueueOperation.IsDone)
                        {
                            m_RequestOperation = m_WebRequestQueueOperation.Result;
                            if (m_RequestOperation.isDone)
                                WebRequestOperationCompleted(m_RequestOperation);
                            else
                                m_RequestOperation.completed += WebRequestOperationCompleted;
                        }
                        else
                        {
                            m_WebRequestQueueOperation.OnComplete += asyncOp =>
                            {
                                m_RequestOperation = asyncOp;
                                m_RequestOperation.completed += WebRequestOperationCompleted;
                            };
                        }
                    }
                    else
                    {
                        m_LoadBundleRequest = AssetBundle.LoadFromFileAsync(m_BundlePath);
                        m_LoadBundleRequest.completed += loadOp =>
                        {
                            if (loadOp is AssetBundleCreateRequest createRequest && createRequest.assetBundle != null)
                            {
                                m_CatalogAssetBundle = createRequest.assetBundle;
                                m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync<TextAsset>();
                                if (m_LoadTextAssetRequest.isDone)
                                    LoadTextAssetRequestComplete(m_LoadTextAssetRequest);
                                m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete;
                            }
                            else
                            {
                                Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}");
                                m_OpInProgress = false;
                            }
                        };
                    }
                }

                private void WebRequestOperationCompleted(AsyncOperation op)
                {
                    UnityWebRequestAsyncOperation remoteReq = op as UnityWebRequestAsyncOperation;
                    var webReq = remoteReq.webRequest;
                    DownloadHandlerAssetBundle downloadHandler = webReq.downloadHandler as DownloadHandlerAssetBundle;
                    if (!UnityWebRequestUtilities.RequestHasErrors(webReq, out UnityWebRequestResult uwrResult))
                    {
                        m_CatalogAssetBundle = downloadHandler.assetBundle;
                        m_LoadTextAssetRequest = m_CatalogAssetBundle.LoadAllAssetsAsync<TextAsset>();
                        if (m_LoadTextAssetRequest.isDone)
                            LoadTextAssetRequestComplete(m_LoadTextAssetRequest);
                        m_LoadTextAssetRequest.completed += LoadTextAssetRequestComplete;
                    }
                    else
                    {
                        Addressables.LogError($"Unable to load dependent bundle from location : {m_BundlePath}");
                        m_OpInProgress = false;
                    }

                    webReq.Dispose();
                }

                void LoadTextAssetRequestComplete(AsyncOperation op)
                {
                    if (op is AssetBundleRequest loadRequest
                        && loadRequest.asset is TextAsset textAsset
                        && textAsset.text != null)
                    {
                        m_CatalogData = JsonUtility.FromJson<ContentCatalogData>(textAsset.text);
                        OnLoaded?.Invoke(m_CatalogData);
                    }
                    else
                    {
                        Addressables.LogError($"No catalog text assets where found in bundle {m_BundlePath}");
                    }

                    Unload();
                    m_OpInProgress = false;
                }

                public bool WaitForCompletion()
                {
                    if (m_LoadBundleRequest.assetBundle == null)
                        return false;

                    return m_LoadTextAssetRequest.asset != null || m_LoadTextAssetRequest.allAssets != null;
                }
            }

            string GetTransformedInternalId(IResourceLocation loc)
            {
                if (m_ProviderInterface.ResourceManager == null)
                    return loc.InternalId;
                return m_ProviderInterface.ResourceManager.TransformInternalId(loc);
            }

            const string kCatalogExt =
#if ENABLE_BINARY_CATALOG
            ".bin";
#else
            ".json";
#endif

            internal string DetermineIdToLoad(IResourceLocation location, IList<object> dependencyObjects, bool disableCatalogUpdateOnStart = false)
            {
                //default to load actual local source catalog
                string idToLoad = GetTransformedInternalId(location);
                if (dependencyObjects != null &&
                    location.Dependencies != null &&
                    dependencyObjects.Count == (int)DependencyHashIndex.Count &&
                    location.Dependencies.Count == (int)DependencyHashIndex.Count)
                {
                    var remoteHash = dependencyObjects[(int)DependencyHashIndex.Remote] as string;
                    m_LocalHashValue = dependencyObjects[(int)DependencyHashIndex.Cache] as string;
                    Addressables.LogFormat("Addressables - ContentCatalogProvider CachedHash = {0}, RemoteHash = {1}.", m_LocalHashValue, remoteHash);

                    if (string.IsNullOrEmpty(remoteHash) || disableCatalogUpdateOnStart) //offline
                    {
#if ENABLE_CACHING
                        if (!string.IsNullOrEmpty(m_LocalHashValue) && !m_Retried && !string.IsNullOrEmpty(Application.persistentDataPath)) //cache exists and not forcing a retry state
                        {
                            idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt);
                        }
                        else
                        {
                            m_LocalHashValue = Hash128.Compute(idToLoad).ToString();
                        }
#else
                        m_LocalHashValue = Hash128.Compute(idToLoad).ToString();
#endif
                    }
                    else //online
                    {
                        if (remoteHash == m_LocalHashValue && !m_Retried) //cache of remote is good and not forcing a retry state
                        {
                            idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt);
                        }
                        else //remote is different than cache, or no cache
                        {
                            idToLoad = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Remote]).Replace(".hash", kCatalogExt);
                            m_RemoteHashValue = remoteHash;
#if ENABLE_CACHING
                            if (!string.IsNullOrEmpty(Application.persistentDataPath))
                                m_LocalDataPath = GetTransformedInternalId(location.Dependencies[(int)DependencyHashIndex.Cache]).Replace(".hash", kCatalogExt);
#endif
                        }
                    }
                }

                return idToLoad;
            }

            private void OnCatalogLoaded(ContentCatalogData ccd)
            {
                Addressables.LogFormat("Addressables - Content catalog load result = {0}.", ccd);
                if (ccd != null)
                {
#if ENABLE_ADDRESSABLE_PROFILER
                    ResourceManagement.Profiling.ProfilerRuntime.AddCatalog(Hash128.Parse(ccd.m_BuildResultHash));
#endif
                    ccd.location = m_ProviderInterface.Location;
                    ccd.localHash = m_LocalHashValue;
                    if (!string.IsNullOrEmpty(m_RemoteHashValue) && !string.IsNullOrEmpty(m_LocalDataPath))
                    {
#if ENABLE_CACHING
                        var dir = Path.GetDirectoryName(m_LocalDataPath);
                        var localCachePath = m_LocalDataPath;
                        Addressables.LogFormat("Addressables - Saving cached content catalog to {0}.", localCachePath);
                        try
                        {
                            if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                                Directory.CreateDirectory(dir);
                            File.WriteAllText(localCachePath, JsonUtility.ToJson(ccd));
                            File.WriteAllText(localCachePath.Replace(kCatalogExt, ".hash"), m_RemoteHashValue);
                        }
                        catch (UnauthorizedAccessException uae)
                        {
                            Addressables.LogWarning($"Did not save cached content catalog. Missing access permissions for location {localCachePath} : {uae.Message}");
                            m_ProviderInterface.Complete(ccd, true, null);
                            return;
                        }
                        catch (Exception e)
                        {
                            string remoteInternalId = GetTransformedInternalId(m_ProviderInterface.Location.Dependencies[(int)DependencyHashIndex.Remote]);
                            var errorMessage = $"Unable to load ContentCatalogData from location {remoteInternalId}. Failed to cache catalog to location {localCachePath}.";
                            ccd = null;
                            m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage, e));
                            return;
                        }
#endif
                        ccd.localHash = m_RemoteHashValue;
                    }
#if ENABLE_CACHING
                    else if (string.IsNullOrEmpty(m_LocalDataPath) && string.IsNullOrEmpty(Application.persistentDataPath))
                    {
                        Addressables.LogWarning($"Did not save cached content catalog because Application.persistentDataPath is an empty path.");
                    }
#endif

                    m_ProviderInterface.Complete(ccd, true, null);
                }
                else
                {
                    var errorMessage = $"Unable to load ContentCatalogData from location {m_ProviderInterface.Location}";
                    if (!m_Retried)
                    {
                        m_Retried = true;

                        //if the prev load path is cache, try to remove cache and reload from remote
                        var cachePath = GetTransformedInternalId(m_ProviderInterface.Location.Dependencies[(int)DependencyHashIndex.Cache]);
                        if (m_ContentCatalogDataLoadOp.LocationName == cachePath.Replace(".hash", kCatalogExt))
                        {
                            try
                            {
#if ENABLE_CACHING
                                File.Delete(cachePath);
#endif
                            }
                            catch (Exception)
                            {
                                errorMessage += $". Unable to delete cache data from location {cachePath}";
                                m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage));
                                return;
                            }
                        }

                        Addressables.LogWarning(errorMessage + ". Attempting to retry...");
                        Start(m_ProviderInterface, m_DisableCatalogUpdateOnStart, m_IsLocalCatalogInBundle);
                    }
                    else
                    {
                        m_ProviderInterface.Complete(ccd, false, new Exception(errorMessage + " on second attempt."));
                    }
                }
            }
        }

        ///<inheritdoc/>
        public override void Provide(ProvideHandle providerInterface)
        {
            if (!m_LocationToCatalogLoadOpMap.ContainsKey(providerInterface.Location))
                m_LocationToCatalogLoadOpMap.Add(providerInterface.Location, new InternalOp());
            m_LocationToCatalogLoadOpMap[providerInterface.Location].Start(providerInterface, DisableCatalogUpdateOnStart, IsLocalCatalogInBundle);
        }
    }
}