﻿/* Copyright (C) Itseez3D, Inc. - All Rights Reserved
* You may not use this file except in compliance with an authorized license
* Unauthorized copying of this file, via any medium is strictly prohibited
* Proprietary and confidential
* UNLESS REQUIRED BY APPLICABLE LAW OR AGREED BY ITSEEZ3D, INC. IN WRITING, SOFTWARE DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED
* See the License for the specific language governing permissions and limitations under the License.
* Written by Itseez3D, Inc. <support@avatarsdk.com>, April 2017
*/

using ItSeez3D.AvatarSdk.Core;
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace ItSeez3D.AvatarSdk.Offline
{
	public class OfflineSdkUtilsImpl : OfflineSdkUtils
	{
		private static readonly string EXTERNAL_RESOURCES_URL = "http://localhost:8000/";

		public new AsyncRequest EnsureSdkResourcesUnpackedAsync(string unpackedResourcesPath)
		{
			AsyncRequest unpackRequest = new AsyncRequest("Verifying resources");
			AvatarSdkMgr.SpawnCoroutine(EnsureSdkResourcesUnpackedFunc(unpackedResourcesPath, unpackRequest));
			return unpackRequest;
		}

		public new AsyncRequest EnsureInitializedAsync(string unpackedResourcesPath, bool showError = false, bool resetResources = false)
		{
			AsyncRequest initializeRequest = new AsyncRequest("Resources initialization");
			AvatarSdkMgr.SpawnCoroutine(EnsureInitializedFunc(unpackedResourcesPath, showError, resetResources, initializeRequest));
			return initializeRequest;
		}

		protected IEnumerator EnsureSdkResourcesUnpackedFunc(string unpackedResourcesPath, AsyncRequest request)
		{
			var resourcesListFilename = "resource_list.txt";
			var externalResourcesListFilename = "external_resources_list.txt";

#if UNITY_EDITOR
			// getting list of local compute sdk resources and saving it to file
			AssetDatabase.Refresh();

			string resourcesDir = PluginStructure.GetPluginDirectoryPath(PluginStructure.OFFLINE_RESOURCES_DIR, PathOriginOptions.FullPath);
			string miscResourcesDir = PluginStructure.GetPluginDirectoryPath(PluginStructure.MISC_OFFLINE_RESOURCES_DIR, PathOriginOptions.FullPath);
			string externalResourcesDir = PluginStructure.GetPluginDirectoryPath(PluginStructure.OFFLINE_EXTERNAL_RESOURCES_DIR, PathOriginOptions.FullPath);

			PluginStructure.CreatePluginDirectory(miscResourcesDir);

			CreateExistedResourcesFilesList(Path.Combine(miscResourcesDir, resourcesListFilename), resourcesDir, miscResourcesDir);

			// getting list of external resources and saving it to file
			CreateExistedResourcesFilesList(Path.Combine(miscResourcesDir, externalResourcesListFilename), externalResourcesDir);

			AssetDatabase.SaveAssets();
			AssetDatabase.Refresh();
#endif
			// verify the integrity of the unpacked resource folder
			bool shouldUnpackResources = false;
			var testFile = Utils.CombinePaths(unpackedResourcesPath, SdkVersion.Version.ToString() + "_copied.test");

			// first of all, check if the indicator file is present in the directory
			if (!File.Exists(testFile))
			{
				Debug.Log("Indicator file doesn't exist");
				shouldUnpackResources = true;
			}

			// get actual list of the unpacked resources
			var unpackedResources = new HashSet<string>();
			foreach (var unpackedResourcePath in Directory.GetFiles(unpackedResourcesPath, "*.*", SearchOption.AllDirectories))
			{
				var unpackedResource = Path.GetFileName(unpackedResourcePath);
				unpackedResources.Add(unpackedResource);
			}

			List<string> resourcesList = new List<string>();
			yield return ReadResourcesList(resourcesListFilename, resourcesList);
			bool unpackedAllResources = resourcesList.All(resource => unpackedResources.Contains(Utils.GetFileName(resource)));
			if (!unpackedAllResources)
			{
				Debug.Log("Not all resources are in local storage.");
				shouldUnpackResources = true;
			}

			List<string> externalResourcesList = new List<string>();
			yield return ReadResourcesList(externalResourcesListFilename, externalResourcesList);
			bool existAllExternalResources = externalResourcesList.All(resource => unpackedResources.Contains(Utils.GetFileName(resource)));
			if (!existAllExternalResources)
			{
				Debug.Log("Not all external resources are in local storage.");
				shouldUnpackResources = true;
			}

			if (shouldUnpackResources)
			{
				int totalResources = 0;
				totalResources += resourcesList.Count;
				totalResources += externalResourcesList.Count;

				Utils.CleanDirectory(unpackedResourcesPath);
				var loadingStartTime = Time.realtimeSinceStartup;
				Debug.LogFormat("Should unpack all resources");
				AsyncRequest unpackRequest = UnpackResourcesAsync(resourcesList.ToArray(), unpackedResourcesPath);
				yield return request.AwaitSubrequest(unpackRequest, (1.0f - request.Progress) * resourcesList.Count / totalResources);
				if (request.IsError)
					yield break;
				Debug.LogFormat("Took {0} seconds to unpack resources", Time.realtimeSinceStartup - loadingStartTime);

				if (externalResourcesList.Count > 0)
				{
					var downloadingStartTime = Time.realtimeSinceStartup;
					Debug.LogFormat("Should download external resources");
					AsyncRequest downloadRequest = DownloadExternalResourcesAsync(externalResourcesList.ToArray(), unpackedResourcesPath);
					yield return request.AwaitSubrequest(downloadRequest, 1.0f);
					if (request.IsError)
						yield break;
					Debug.LogFormat("Took {0} seconds to download external resources", Time.realtimeSinceStartup - downloadingStartTime);
				}
			}

			Debug.LogFormat("Unpacked and downloaded all resources!");
			if (!string.IsNullOrEmpty(testFile))
				File.WriteAllText(testFile, "unpacked!");

			request.Progress = 1.0f;
			request.IsDone = true;
		}

		protected IEnumerator EnsureInitializedFunc(string unpackedResourcesPath, bool showError, bool resetResources, AsyncRequest request)
		{
			IsInitializing = true;

			if (resetResources)
			{
				Utils.CleanDirectory(unpackedResourcesPath);
#if UNITY_EDITOR
				string miscResourcesDir = PluginStructure.GetPluginDirectoryPath(PluginStructure.MISC_OFFLINE_RESOURCES_DIR, PathOriginOptions.RelativeToAssetsFolder);
				AssetDatabase.DeleteAsset(miscResourcesDir);
				PluginStructure.CreatePluginDirectory(miscResourcesDir);
				AssetDatabase.Refresh();
#endif
			}

			string clientId = null, clientSecret = null;
			var accessCredentials = AuthUtils.LoadCredentials();
			if (accessCredentials != null)
			{
				clientId = accessCredentials.clientId;
				clientSecret = accessCredentials.clientSecret;
			}

			var offlineSdkInitializer = new OfflineSdkInitializer();
			string miscResourcesPath = PluginStructure.GetPluginDirectoryPath(PluginStructure.MISC_OFFLINE_RESOURCES_DIR, PathOriginOptions.FullPath);
			yield return offlineSdkInitializer.Run(miscResourcesPath, SdkVersion.Version.ToString(), NetworkUtils.rootUrl, clientId, clientSecret);
			if (!offlineSdkInitializer.Success)
			{
				if (showError)
					Utils.DisplayWarning("Could not initialize local compute SDK!", "Error message: \n" + offlineSdkInitializer.LastError);
				request.SetError(string.Format("Could not initialize local compute SDK!", "Error message: {0}", offlineSdkInitializer.LastError));
				yield break;
			}

			yield return EnsureSdkResourcesUnpackedFunc(unpackedResourcesPath, request);

			IsInitializing = false;
		}

		protected AsyncRequest DownloadExternalResourcesAsync(string[] resourceList, string unpackedResourcesPath)
		{
			AsyncRequest downloadRequest = new AsyncRequest("Downloading resources");
			AvatarSdkMgr.SpawnCoroutine(DownloadExternalResourcesFunc(resourceList, unpackedResourcesPath, downloadRequest));
			return downloadRequest;
		}

		protected IEnumerator DownloadExternalResourcesFunc(string[] resourceList, string unpackedResourcesPath, AsyncRequest request)
		{
			int nSimultaneously = 20;
			for (int i = 0; i < resourceList.Length; i += nSimultaneously)
			{
				Dictionary<string, UnityWebRequest> webRequests = new Dictionary<string, UnityWebRequest>();
				for (int j = 0; j < nSimultaneously && i + j < resourceList.Length; ++j)
				{
					var resource = resourceList[i + j];
					UnityWebRequest downloadRequest = UnityWebRequest.Get(EXTERNAL_RESOURCES_URL + resource);
					downloadRequest.SendWebRequest();
					webRequests.Add(resource, downloadRequest);
				}

				yield return AsyncUtils.AwaitAll(webRequests.Values.ToArray());

				var copyRequests = new List<AsyncRequestThreaded<string>>();
				foreach (var webRequestPair in webRequests)
				{
					//File.WriteAllBytes(Path.Combine(unpackedResourcesPath, Utils.GetFileName(webRequestPair.Key)), webRequestPair.Value.downloadHandler.data);
					byte[] resourceBytes = webRequestPair.Value.downloadHandler.data;
					CopyPipelineResources(webRequestPair.Key, unpackedResourcesPath, resourceBytes,
									(string path, byte[] data) => {
										copyRequests.Add(new AsyncRequestThreaded<string>(() => {
											File.WriteAllBytes(path, resourceBytes);
											return path;
										}));
									});
				}
				yield return AsyncUtils.AwaitAll(copyRequests.ToArray());

				request.Progress = (i + 1) / (float)resourceList.Length;
				Debug.LogFormat("Downloading resources... {0}%", (int)request.ProgressPercent);
			}

			request.IsDone = true;
		}

		protected void CreateExistedResourcesFilesList(string listFilename, params string[] dirs)
		{
			var resourceFiles = new List<string>();
			foreach (var dir in dirs)
			{
				GetFileNamesInDirectory(ref resourceFiles, dir, "", true, resourceExtension);
			}
			File.WriteAllLines(listFilename, resourceFiles.ToArray());
		}

		protected IEnumerator ReadResourcesList(string resourcesListFilename, List<string> resourcesNames)
		{
			// read list of all resources files
			UnityEngine.Object resourceListObject = null;
			var resourceListPath = GetResourcePathInAssets(resourcesListFilename);
			if (Utils.IsDesignTime())
				resourceListObject = Resources.Load(resourceListPath);
			else
			{
				var resourceListRequest = Resources.LoadAsync(resourceListPath);
				yield return resourceListRequest;
				resourceListObject = resourceListRequest.asset;
			}

			var resourceListAsset = resourceListObject as TextAsset;
			if (resourceListAsset == null)
			{
				Debug.LogErrorFormat("Could not read the list of resources from {0}", resourcesListFilename);
				yield break;
			}

			resourcesNames.AddRange(resourceListAsset.text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries));
		}
	}
}