using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEditor.PackageManager.UI;

namespace UnityEditor.Timeline
{
    [InitializeOnLoad]
    class SampleDependencyImporter : IPackageManagerExtension
    {
        static readonly string k_DependenciesDialogTitle = L10n.Tr("Import Sample Package Dependencies");
        static readonly string k_DependenciesDialogMessage = L10n.Tr("These samples contain package dependencies that your project does not have: ");
        static readonly string k_DependenciesDialogOK = L10n.Tr("Import samples and their dependencies");
        static readonly string k_DependenciesDialogCancel = L10n.Tr("Import samples without their dependencies");
        static readonly string k_NewLine = "\n";

        const string k_TimelinePackageName = "com.unity.timeline";

        PackageManager.PackageInfo m_PackageInfo;
        IEnumerable<Sample> m_Samples;
        SampleConfiguration m_SampleConfiguration;
        PackageChecker m_PackageChecker = new PackageChecker();

        static SampleDependencyImporter() => PackageManagerExtensions.RegisterExtension(new SampleDependencyImporter());
        UnityEngine.UIElements.VisualElement IPackageManagerExtension.CreateExtensionUI() => default;

        public void OnPackageAddedOrUpdated(PackageManager.PackageInfo packageInfo) => m_PackageChecker.RefreshPackageCache();
        public void OnPackageRemoved(PackageManager.PackageInfo packageInfo) => m_PackageChecker.RefreshPackageCache();

        /// <summary>
        /// Called when the package selection changes in the Package Manager window.
        /// It loads Timeline package info and configuration, when Timeline is selected.
        /// </summary>
        public void OnPackageSelectionChange(PackageManager.PackageInfo packageInfo)
        {
            bool timelinePackageInfo = packageInfo != null && packageInfo.name.StartsWith(k_TimelinePackageName);
            if (m_PackageInfo == null && timelinePackageInfo)
            {
                m_PackageInfo = packageInfo;
                m_Samples = Sample.FindByPackage(packageInfo.name, packageInfo.version);
                if (TryLoadSampleConfiguration(m_PackageInfo, out m_SampleConfiguration))
                    SamplePostprocessor.AssetImported += LoadAssetDependencies;
            }
            else if (!timelinePackageInfo)
            {
                m_PackageInfo = null;
                SamplePostprocessor.AssetImported -= LoadAssetDependencies;
            }
        }

        /// <summary>Load the sample configuration for the specified package, if one is available.</summary>
        static bool TryLoadSampleConfiguration(PackageManager.PackageInfo packageInfo, out SampleConfiguration configuration)
        {
            var configurationPath = $"{packageInfo.assetPath}/Samples~/sampleDependencies.json";
            if (File.Exists(configurationPath))
            {
                var configurationText = File.ReadAllText(configurationPath);
                configuration = JsonUtility.FromJson<SampleConfiguration>(configurationText);
                return true;
            }
            configuration = null;
            return false;
        }

        AddRequest m_PackageAddRequest;
        int m_PackageDependencyIndex;
        List<string> m_PackageDependencies = new List<string>();

        void LoadAssetDependencies(string assetPath)
        {
            if (!PackageManagerUIOpen())
            {
                SamplePostprocessor.AssetImported -= LoadAssetDependencies;
                return;
            }

            if (m_SampleConfiguration != null)
            {
                var assetsImported = false;
                foreach (var sample in m_Samples)
                {
                    if (assetPath.EndsWith(sample.displayName))
                    {
                        var sampleEntry = m_SampleConfiguration.GetEntry(sample);
                        if (sampleEntry != null)
                        {
                            // Import common asset dependencies
                            assetsImported = ImportAssetDependencies(
                                m_PackageInfo, m_SampleConfiguration.SharedAssetDependencies, out var sharedDestinations);

                            // Import sample-specific asset dependencies
                            assetsImported |= ImportAssetDependencies(
                                m_PackageInfo, sampleEntry.AssetDependencies, out var localDestinations);


                            // Import common amd sample specific package dependencies
                            m_PackageDependencyIndex = 0;
                            m_PackageDependencies = new List<string>(m_SampleConfiguration.SharedPackageDependencies);
                            m_PackageDependencies.AddRange(sampleEntry.PackageDependencies);

                            if (m_PackageDependencies.Count != 0 &&
                                DoDependenciesNeedToBeImported(out var dependenciesToImport))
                            {
                                if (PromptUserImportDependencyConfirmation(dependenciesToImport))
                                {
                                    // only import dependencies that are missing
                                    m_PackageDependencies = dependenciesToImport;
                                    // Import package dependencies using the editor update loop, because
                                    // adding packages need to be done in sequence one after the other
                                    EditorApplication.update += ImportPackageDependencies;
                                }
                            }
                        }
                        break;
                    }
                }

                if (assetsImported)
                    AssetDatabase.Refresh();
            }

            // local functions
            bool DoDependenciesNeedToBeImported(out List<string> packagesToImport)
            {
                packagesToImport = new List<string>();
                foreach (var packageName in m_PackageDependencies)
                {
                    if (!m_PackageChecker.ContainsPackage(packageName))
                        packagesToImport.Add(packageName);
                }

                return packagesToImport.Count != 0;
            }

            void ImportPackageDependencies()
            {
                if (m_PackageAddRequest != null && !m_PackageAddRequest.IsCompleted)
                    return; // wait while we have a request pending

                if (m_PackageDependencyIndex < m_PackageDependencies.Count)
                    m_PackageAddRequest = Client.Add(m_PackageDependencies[m_PackageDependencyIndex++]);
                else
                {
                    m_PackageDependencies.Clear();
                    m_PackageAddRequest = null;
                    EditorApplication.update -= ImportPackageDependencies;
                }
            }

            static bool ImportAssetDependencies(PackageManager.PackageInfo packageInfo, string[] paths, out List<string> destinations)
            {
                destinations = new List<string>();
                if (paths == null)
                    return false;

                var assetsImported = false;
                foreach (var path in paths)
                {
                    var dependencyPath = Path.GetFullPath($"Packages/{packageInfo.name}/Samples~/{path}");
                    if (Directory.Exists(dependencyPath))
                    {
                        var copyTo =
                            $"{Application.dataPath}/Samples/{packageInfo.displayName}/{packageInfo.version}/{path}";
                        CopyDirectory(dependencyPath, copyTo);
                        destinations.Add(copyTo);
                        assetsImported = true;
                    }
                }

                return assetsImported;
            }

            static bool PromptUserImportDependencyConfirmation(List<string> dependencies)
            {
                var aggregate = dependencies.Aggregate(string.Empty, (current, dependency) => current + (dependency + k_NewLine));
                return EditorUtility.DisplayDialog(
                    k_DependenciesDialogTitle,
                    $"{k_DependenciesDialogMessage}{k_NewLine}{aggregate}",
                    k_DependenciesDialogOK,
                    k_DependenciesDialogCancel);
            }
        }

        static bool PackageManagerUIOpen() => PackageManagerWindow.instance != null;

        /// <summary>Copies a directory from the source to target path. Overwrites existing directories.</summary>
        static void CopyDirectory(string sourcePath, string targetPath)
        {
            // Verify source directory
            var source = new DirectoryInfo(sourcePath);
            if (!source.Exists)
                throw new DirectoryNotFoundException($"{sourcePath}  directory not found");

            // Delete pre-existing directory at target path
            var target = new DirectoryInfo(targetPath);
            if (target.Exists)
                target.Delete(true);

            Directory.CreateDirectory(targetPath);

            // Copy all files to target path
            foreach (FileInfo file in source.GetFiles())
            {
                var newFilePath = Path.Combine(targetPath, file.Name);
                file.CopyTo(newFilePath);
            }

            // Recursively copy all subdirectories
            foreach (DirectoryInfo child in source.GetDirectories())
            {
                var newDirectoryPath = Path.Combine(targetPath, child.Name);
                CopyDirectory(child.FullName, newDirectoryPath);
            }
        }

        /// <summary>An AssetPostProcessor which will raise an event when a new asset is imported.</summary>
        class SamplePostprocessor : AssetPostprocessor
        {
            public static event Action<string> AssetImported;

            static void OnPostprocessAllAssets(string[] importedAssets,
                string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
            {
                if (AssetImported == null)
                    return;

                foreach (var importedAsset in importedAssets)
                    AssetImported.Invoke(importedAsset);
            }
        }

        /// <summary>A configuration class defining information related to samples for the package.</summary>
        [Serializable]
        class SampleConfiguration
        {
            /// <summary>This class defines the path and dependencies for a specific sample.</summary>
            [Serializable]
            public class SampleEntry
            {
                public string Path;
                public string[] AssetDependencies;
                public string[] PackageDependencies;
            }

            public string[] SharedAssetDependencies;
            public string[] SharedPackageDependencies;
            public SampleEntry[] SampleEntries;

            public SampleEntry GetEntry(Sample sample) =>
                SampleEntries?.FirstOrDefault(t => sample.resolvedPath.EndsWith(t.Path));
        }

        class PackageChecker
        {
            ListRequest m_Request;
            PackageCollection m_Packages;

            public PackageChecker()
            {
                RefreshPackageCache();
            }

            public void RefreshPackageCache()
            {
                if (m_Request != null && !m_Request.IsCompleted)
                    return; // need to wait for previous request to finish

                m_Request = Client.List(true);
                EditorApplication.update += WaitForRequestToComplete;
            }

            void WaitForRequestToComplete()
            {
                if (m_Request.IsCompleted)
                {
                    if (m_Request.Status == StatusCode.Success)
                        m_Packages = m_Request.Result;
                    EditorApplication.update -= WaitForRequestToComplete;
                }
            }

            public bool ContainsPackage(string packageName)
            {
                // Check each package and package dependency for packageName
                foreach (var package in m_Packages)
                {
                    if (string.Compare(package.name, packageName) == 0)
                        return true;

                    if (package.dependencies.Any(dependencyInfo => string.Compare(dependencyInfo.name, packageName) == 0))
                        return true;
                }

                return false;
            }
        }
    }
}