﻿/* 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 System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ItSeez3D.AvatarSdk.Core;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

namespace ItSeez3D.AvatarSdk.Offline
{
	public class OfflineSdkUtils
	{
		private static OfflineSdkUtilsImpl utilsImpl = new OfflineSdkUtilsImpl();

		protected static readonly string offlineResourcesDir = "bin";
		protected static readonly string resourceExtension = ".bytes";

		public const string HeadResourcesSubDirectoryName = "head_resources";
		public const string FaceResourcesSubDirectoryName = "face_resources";

		public static bool IsInitializing { get; protected set; }

		/// <summary>
		/// Build paths where resource should be placed. Resources from head_resources go to head_resources, 
		/// resources from face_resources go to face_resources, resources from root go to both directories
		/// </summary>
		/// <param name="srcResource">Relative path to source resource file</param>
		/// <returns>Paths where resource should be placed</returns>
		protected static IEnumerable<string> BuildDestinationResourcePath(string srcResource)
		{
			var partsOfPath = srcResource.Split(new char[] { '/', '\\' });
			var rootDir = partsOfPath.FirstOrDefault();
			var fileName = partsOfPath.LastOrDefault();
			if (AvatarSdkMgr.Settings.SeparateHeadAndFaceResources)
			{
				switch (rootDir)
				{
					case HeadResourcesSubDirectoryName:
					case FaceResourcesSubDirectoryName:
						yield return Path.Combine(rootDir, fileName);
						break;
					default:
						yield return Path.Combine(HeadResourcesSubDirectoryName, fileName);
						yield return Path.Combine(FaceResourcesSubDirectoryName, fileName);
						break;
				}
			}
			else
				yield return fileName;
		}

		/// <summary>
		/// Copy resource to the places where it should be. 
		/// </summary>
		/// <param name="srcResoursePath">Relative path to resource file</param>
		/// <param name="dstResourceDirPath">Path to directory where resources are placed</param>
		/// <param name="bytes">Data read from resource file</param>
		/// <param name="copyFunc">Function that executes copying and accompanying operations</param>
		protected static void CopyPipelineResources(string srcResoursePath, string dstResourceDirPath, byte[] bytes, Action<string, byte[]> copyFunc)
		{
			foreach (string dstFilePath in BuildDestinationResourcePath(srcResoursePath))
			{
				var subDir = Path.GetDirectoryName(dstFilePath);
				var dstSubDir = Path.Combine(dstResourceDirPath, subDir);
				if (!Directory.Exists(dstSubDir))
				{
					Directory.CreateDirectory(dstSubDir);
				}
				Debug.LogFormat("Copying {0}...", dstFilePath);
				copyFunc(Path.Combine(dstResourceDirPath, dstFilePath), bytes);
			}
		}

		protected static void RemoveExistingUnpackedResources (string unpackedResourcesPath)
		{
			if (Directory.Exists (unpackedResourcesPath)) {
				// remove all the existing data
				Directory.Delete (unpackedResourcesPath, true);
			}
			Directory.CreateDirectory (unpackedResourcesPath);
		}

		protected static string GetResourcePathInAssets(string resource)
		{
			string path = offlineResourcesDir + "/" + Path.Combine(Path.GetDirectoryName(resource), Path.GetFileNameWithoutExtension(resource));
			return path;
		}

		protected static AsyncRequest UnpackResourcesAsync(string[] resourceList, string unpackedResourcesPath)
		{
			AsyncRequest unpackRequest = new AsyncRequest("Unpacking resources");
			AvatarSdkMgr.SpawnCoroutine(UnpackResourcesFunc(resourceList, unpackedResourcesPath, unpackRequest));
			return unpackRequest;
		}

		protected static IEnumerator UnpackResourcesFunc(string[] resourceList, string unpackedResourcesPath, AsyncRequest unpackRequest)
		{
			if (Utils.IsDesignTime())
			{
				// Resources.LoadAsync does not work in Editor in Unity 5.6, hence the special non-async version
				for (int i = 0; i < resourceList.Length; i++)
				{
					var resourceObject = Resources.Load(GetResourcePathInAssets(resourceList[i]));
					var asset = resourceObject as TextAsset;

					CopyPipelineResources(resourceList[i], unpackedResourcesPath, asset.bytes,
						(string path, byte[] data) => {
							File.WriteAllBytes(path, data);
						});

					unpackRequest.Progress = (i + 1) / (float)resourceList.Length;

					yield return null;  // avoid blocking the main thread
				}
			}
			else
			{
				// load several resources at a time to reduce loading time
				int nSimultaneously = 20;
				for (int i = 0; i < resourceList.Length; i += nSimultaneously)
				{
					var resourceRequestsAsync = new Dictionary<string, ResourceRequest>();
					for (int j = 0; j < nSimultaneously && i + j < resourceList.Length; ++j)
					{
						var resource = resourceList[i + j];
						var request = Resources.LoadAsync(GetResourcePathInAssets(resource));
						resourceRequestsAsync.Add(resource, request);
					}

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

					var copyRequests = new List<AsyncRequestThreaded<string>>();
					foreach (var request in resourceRequestsAsync)
					{
						var asset = request.Value.asset as TextAsset;
						if (asset == null)
						{
							string errorMessage = "Asset is null! Could not unpack one of the resources!";
							Debug.LogError(errorMessage);
							unpackRequest.SetError(errorMessage);
							yield break;
						}
						else
						{
							var assetBytes = asset.bytes;
							CopyPipelineResources(request.Key, unpackedResourcesPath, assetBytes,
								(string path, byte[] data) => {
									copyRequests.Add(new AsyncRequestThreaded<string>(() => {
										File.WriteAllBytes(path, assetBytes);
										return path;
									}));
								});
						}
					}

					yield return AsyncUtils.AwaitAll(copyRequests.ToArray());

					foreach (var request in resourceRequestsAsync)
					{
						var asset = request.Value.asset as TextAsset;
						if (asset != null)
							Resources.UnloadAsset(asset);
					}

					unpackRequest.Progress = (i + 1) / (float)resourceList.Length;
				}
			}

			Resources.UnloadUnusedAssets();
			GC.Collect();
			unpackRequest.IsDone = true;
		}

		/// <summary>
		/// Finds files in directory and adds their names to the list.
		/// </summary>
		/// <param name="fileNames">Output list with filenames</param>
		/// <param name="dir">Directory where files will be searched</param>
		/// <param name="relativeDir">Relative directory to concatenate with filename</param>
		/// <param name="includeSubDir">True if need to look up files in subdirs</param>
		/// <param name="extension">Include only files with the given extension</param>
		protected static void GetFileNamesInDirectory(ref List<string> fileNames, string dir, string relativeDir, bool includeSubDir = true, string extension = "")
		{
			if (!Directory.Exists(dir))
				return;

			foreach (var filePath in Directory.GetFiles(dir))
			{
				var filename = Path.GetFileName(filePath);
				if (filename.EndsWith(extension))
					fileNames.Add(Path.Combine(relativeDir, filename));
			}

			if (includeSubDir)
			{
				foreach (var subdir in Directory.GetDirectories(dir))
					GetFileNamesInDirectory(ref fileNames, subdir, Path.Combine(relativeDir, Path.GetFileName(subdir)), true, extension);
			}
		}

		public static AsyncRequest EnsureSdkResourcesUnpackedAsync(string unpackedResourcesPath)
		{
			return utilsImpl.EnsureSdkResourcesUnpackedAsync(unpackedResourcesPath);
		}

		public static AsyncRequest EnsureInitializedAsync (string unpackedResourcesPath, bool showError = false, bool resetResources = false)
		{
			return utilsImpl.EnsureInitializedAsync(unpackedResourcesPath, showError, resetResources);
		}
	}
}
