using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.PackageManager.UI;
using UnityEngine;
using UnityEngine.UIElements;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;

/// <remarks>
/// In the package.json, an array can be added after the path variable of the sample. The path should start from the Packages/ folder, as such:
/// "samples": [
/// {
///     "displayName": "Sample name",
///     "description": "Sample description",
///     "path": "Samples~/Stuff",
///     "dependencies": 
///         [
///             "com.unity.render-pipelines.core/Samples~/CommonMeshes",
///             "com.unity.render-pipelines.core/Samples~/CommonTextures",
///             "com.unity.render-pipelines.universal/Samples~/CommonURPMaterials",
///             "com.unity.render-pipelines.high-definition/Samples~/CommonHDRPMaterials",
///         ]
/// },
/// {
/// </remarks>

[InitializeOnLoad]
internal class SampleDependencyImporter : IPackageManagerExtension
{
    internal static SampleDependencyImporter instance { get; private set; }

    static SampleDependencyImporter()
    {
        instance = new SampleDependencyImporter();
        PackageManagerExtensions.RegisterExtension(instance);
    }

    bool importingTextMeshProEssentialResources = false;

    PackageInfo m_PackageInfo;
    SampleList m_SampleList;
    List<Sample> m_Samples;

    VisualElement injectingElement;
    VisualElement _panelRoot;
    VisualElement panelRoot
    {
        get
        {
            _panelRoot ??= injectingElement?.panel?.visualTree;
            return _panelRoot;
        }
    }

    /// <summary>
    /// Use the extension UI to "inject" an invisible element in package manager UI
    /// that will serve as a base to hook up additional logic to the import buttons.
    /// </summary>
    VisualElement IPackageManagerExtension.CreateExtensionUI()
    {
        injectingElement = new VisualElement();
        injectingElement.style.display = DisplayStyle.None;

        // This callback is called once the element is added to the UI, at this point we should have access to rest of the elements.
        injectingElement.RegisterCallback<AttachToPanelEvent>((callback) => {
            //Force clear the cached elements to fetch those from the newly openned window
            _panelRoot = null;
            samplesButton = null;

            RefreshSampleButtons();
        });

        return injectingElement;
    }

    Button samplesButton;
    const string samplesButtonName = "samplesButton";
    const string sampleContainerClassName = "sampleContainer";
    const string importButtonClassName = "importButton";
    const string injectedButtonClassName = "importWithDependenciesButton";

    void RefreshSampleButtons()
    {
        if (injectingElement == null || m_PackageInfo == null || m_SampleList == null || panelRoot == null)
            return;

        // Call refresh of samples and button injection when switching to the "Samples" tab.
        if (samplesButton == null)
        {
            samplesButton = panelRoot.Q<Button>(name: samplesButtonName);
            if (samplesButton != null)
                samplesButton.clicked += RefreshSampleButtons;
        }

        // Get all of the samples container elements.
        var query = panelRoot.Query(className: sampleContainerClassName);
        query.Build();
        var sampleContainers = query.ToList();

        var bound = Mathf.Min(sampleContainers.Count, m_SampleList.samples.Length);

        for (int i=0; i<bound; i++)
        {
            // Check if the sample has dependencies, if not just skip the injection.
            var sampleInfo = m_SampleList.samples[i];
            if (sampleInfo.dependencies == null || sampleInfo.dependencies.Length == 0)
                continue;

            // Inject the button if not already.
            var sampleContainer = sampleContainers[i];
            var injectedButton = sampleContainer.Q<Button>(className: injectedButtonClassName);

            if (injectedButton == null)
            {
                // Get and hide the original import button.
                var importButton = sampleContainer.Q<Button>(className: importButtonClassName);
                importButton.style.display = DisplayStyle.None;

                // Create a new button copying the original one with our additional class.
                injectedButton = new Button();
                foreach (var c in importButton.GetClasses())
                    injectedButton.AddToClassList(c);
                injectedButton.AddToClassList(injectedButtonClassName);
                injectedButton.text = importButton.text;

                // Add the new button at the same place as the original one.
                importButton.parent.Insert(importButton.parent.IndexOf(importButton), injectedButton);

                // Need to copy i for the lambda.
                var index = i;
                // On click of the imported button, import the dependencies first then call the original button logic.
                injectedButton.clicked += () => {
                    ImportSampleDependencies(index);

                    using (var ev = NavigationSubmitEvent.GetPooled())
                    {
                        ev.target = importButton;
                        importButton.SendEvent(ev);
                    }
                };
            }
        };
    }

    public void OnPackageAddedOrUpdated(PackageInfo packageInfo) {}
    public void OnPackageRemoved(PackageInfo packageInfo) {}

    /// <summary>
    /// Called when the package selection changes in the Package Manager window.
    /// The dependency importer will track the selected package and its sample configuration.
    /// </summary>
    void IPackageManagerExtension.OnPackageSelectionChange(PackageInfo packageInfo)
    {
        m_PackageInfo = packageInfo;

        if (packageInfo == null)
            return;

        // Only trigger the import if the package has samples. 
        if (new List<Sample>(Sample.FindByPackage(packageInfo.name, packageInfo.version)).Count > 0)
        {
            TryLoadSampleConfiguration(m_PackageInfo, out m_SampleList);
        }
        else
        {
            m_PackageInfo = null;
            m_SampleList = null;
        }

        RefreshSampleButtons();
    }

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

            return true;
        }

        configuration = null;
        return false;
    }

    /// <summary>
    /// Imports a sample dependencies by sample index in the list of samples of the package.
    /// </summary>
    void ImportSampleDependencies( int sampleIndex )
    {
        if (m_SampleList != null && m_SampleList.samples != null && m_SampleList.samples.Length > sampleIndex)
            ImportSampleDependencies(m_SampleList.samples[sampleIndex]);
    }

    /// <summary>
    /// Imports a sample dependencies by sample information.
    /// </summary>
    void ImportSampleDependencies(SampleInformation sampleInformation )
    {
        if (sampleInformation == null) return;

        bool assetsImported = ImportDependencies(sampleInformation.dependencies);
        ImportTextMeshProEssentialResources();

        if ( assetsImported)
            AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
    }


    /// <summary>
    /// Imports a sample dependencies from PackageInfo and Sample struct.
    /// </summary>
    internal void ImportSampleDependencies( PackageInfo packageInfo, Sample sample )
    {
        if (TryLoadSampleConfiguration(packageInfo, out var sampleList))
        {
            if (sampleList.samples != null && sampleList.samples.Length > 0)
            {
                for (int i=0; i<sampleList.samples.Length; i++)
                {
                    if ( sampleList.samples[i].displayName == sample.displayName )
                    {
                        ImportSampleDependencies(sampleList.samples[i]);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Import TMP Essential Resources folder to avoid having a popup on scene open.
    /// </summary>
    public void ImportTextMeshProEssentialResources()
    {
        string essentialResourcesFolder = Path.GetFullPath("Assets/TextMesh Pro");
        bool essentialResourcesImported = Directory.Exists(essentialResourcesFolder);
        // If the folder exists and we were importing, this means the import is done. 
        if (importingTextMeshProEssentialResources && essentialResourcesImported)
            importingTextMeshProEssentialResources = false;

        string packageFullPath = Path.GetFullPath("Packages/com.unity.ugui");
        if (Directory.Exists(packageFullPath) && !importingTextMeshProEssentialResources && !essentialResourcesImported)
        {
            importingTextMeshProEssentialResources = true;
            AssetDatabase.ImportPackage(packageFullPath + "/Package Resources/TMP Essential Resources.unitypackage", interactive: false);
        }
    }
    
    /// <summary>
    /// Returns the properties of the samples based on the sample displayName
    /// </summary>
    public SampleInformation GetSampleInformation(string sampleName)
    {
        foreach(SampleInformation sample in m_SampleList.samples)
        {
            if(sample.displayName == sampleName)
            {
                return sample;
            }
        }
        
        return null;
    }

    /// <summary>
    /// Imports specified dependencies from the package into the project.
    /// </summary>
    static bool ImportDependencies(string[] paths)
    {
        if (paths == null)
            return false;

        var assetsImported = false;
        for (int i = 0; i < paths.Length; ++i)
        {
            var dependencyPath = Path.GetFullPath($"Packages/{paths[i]}");
            if (Directory.Exists(dependencyPath))
            {
                //Getting the PackageInfo from the path to be able to retrieve the displayName and version of the package. 
                PackageInfo currentDependencyPackageInfo = PackageInfo.FindForAssetPath(dependencyPath);
                //Split the path from the package folder into an array of folders
                string[] foldersArray = paths[i].Split('/'); 
                //Last folder is the one we want to copy
                string folderToCopyName = foldersArray[Mathf.Max(foldersArray.Length-1,0)]; 
              
                CopyDirectory(dependencyPath, $"{Application.dataPath}/Samples/{currentDependencyPackageInfo.displayName}/{currentDependencyPackageInfo.version}/{folderToCopyName}");
                assetsImported = true;
            }
            else
            {
                Debug.LogError($"The dependency located at {dependencyPath} does not exists and has not been imported. Make sure the package of the dependency is imported in the project.");
            }
        }

        return assetsImported;
    }

    /// <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);
        }
    }
}