// Microsoft
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;

// Unity
using UnityEditor.Android;

// GUPS - AntiCheat
using GUPS.AntiCheat.Settings;

namespace GUPS.AntiCheat.Editor.Build
{
    /// <summary>
    /// Handles post-processing of the Gradle Android project after it is generated by Unity.
    /// Implements the <see cref="IPostGenerateGradleAndroidProject"/> interface.
    /// </summary>
    /// <remarks>
    /// This class modifies the AndroidManifest.xml file of the generated Gradle project by adding
    /// specific `<package>` elements under the `<queries>` section to ensure compatibility with Android's
    /// runtime permissions model.
    /// </remarks>
    internal class PostProcessAndroidBuild : IPostGenerateGradleAndroidProject
    {
        /// <summary>
        /// Gets the callback order of this postprocessor.
        /// </summary>
        /// <remarks>
        /// The callback order is set to a high value (<see cref="Int32.MaxValue"/> - 1) to ensure
        /// that this postprocessor is executed near the very end of the post-processing pipeline.
        /// This allows it to modify the AndroidManifest.xml as the final step without overwriting
        /// changes made by other postprocessors.
        /// </remarks>
        public int callbackOrder => Int32.MaxValue - 1;

        /// <summary>
        /// Retrieves the path to the `AndroidManifest.xml` file within the generated Gradle project.
        /// </summary>
        /// <param name="_BasePath">The base path of the Gradle project.</param>
        /// <returns>The full file path to the `AndroidManifest.xml` file.</returns>
        /// <remarks>
        /// The method constructs the path by combining the base path with the relative path to the manifest file,
        /// which is typically located in `src/main/AndroidManifest.xml`.
        /// </remarks>
        private String GetManifestPath(String _BasePath)
        {
            return Path.Combine(_BasePath, "src", "main", "AndroidManifest.xml");
        }

        /// <summary>
        /// Processes the generated Gradle Android project after it is created by Unity.
        /// </summary>
        /// <param name="_BasePath">The base path of the generated Gradle project.</param>
        /// <remarks>
        /// This method performs the following steps:
        /// <list type="number">
        /// <item>Locates the `AndroidManifest.xml` file in the Gradle project.</item>
        /// <item>Loads the manifest using the <see cref="AndroidManifest"/> class.</item>
        /// <item>Adds `<package>` elements under the `<queries>` section for applications specified in the global settings.</item>
        /// <item>Saves the updated manifest back to the file system.</item>
        /// </list>
        /// </remarks>
        public void OnPostGenerateGradleAndroidProject(String _BasePath)
        {
#if UNITY_ANDROID

            // Get the path to the AndroidManifest.xml file.
            String var_ManifestPath = this.GetManifestPath(_BasePath);

            // Load the AndroidManifest.xml file.
            AndroidManifest var_Manifest = new AndroidManifest(var_ManifestPath);

            // Add the packages into the AndroidManifest.xml file.
            List<String> var_AppPackages = this.GetAppPackagesToFind();

            for (int i = 0; i < var_AppPackages.Count; i++)
            {
                var_Manifest.AddQueryPackage(var_AppPackages[i]);
            }

            // Save the changes to the AndroidManifest.xml file.
            var_Manifest.Save();

#endif
        }

        /// <summary>
        /// Retrieves the list of application package names to add to the `<queries>` section of the manifest.
        /// </summary>
        /// <returns>A list of application package names, or an empty list if no global settings are available.</returns>
        /// <remarks>
        /// The list of applications is determined by accessing the global settings instance and retrieving
        /// the `Android_BlacklistedApplications` property. If the global settings are unavailable, an empty
        /// list is returned.
        /// </remarks>
        private List<String> GetAppPackagesToFind()
        {
            return GlobalSettings.Instance?.Android_BlacklistedApplications ?? new List<String>();
        }

        /// <summary>
        /// Represents an Android XML document that handles Android-specific XML namespace and file operations.
        /// </summary>
        /// <remarks>
        /// This class extends XmlDocument to provide specialized functionality for working with Android XML files,
        /// including proper namespace management and file handling.
        /// </remarks>
        internal class AndroidXmlDocument : XmlDocument
        {
            /// <summary>
            /// The file path of the Android XML document.
            /// </summary>
            private String filePath;

            /// <summary>
            /// The namespace manager for handling XML namespaces in the document.
            /// </summary>
            protected XmlNamespaceManager namespaceManager;

            /// <summary>
            /// The standard Android XML namespace URI.
            /// </summary>
            public readonly String AndroidXmlNamespace = "http://schemas.android.com/apk/res/android";

            /// <summary>
            /// Initializes a new instance of the AndroidXmlDocument class.
            /// </summary>
            /// <param name="_Path">The file path of the Android XML document to load.</param>
            /// <remarks>
            /// Loads the XML document from the specified path and initializes the namespace manager
            /// with the Android XML namespace.
            /// </remarks>
            public AndroidXmlDocument(String _Path)
            {
                this.filePath = _Path;
                using (var var_Reader = new XmlTextReader(this.filePath))
                {
                    var_Reader.Read();
                    this.Load(var_Reader);
                }
                this.namespaceManager = new XmlNamespaceManager(this.NameTable);
                this.namespaceManager.AddNamespace("android", this.AndroidXmlNamespace);
            }

            /// <summary>
            /// Saves the XML document to its original file path.
            /// </summary>
            /// <remarks>
            /// Uses the file path specified during initialization to save the document.
            /// </remarks>
            public void Save()
            {
                this.SaveAt(this.filePath);
            }

            /// <summary>
            /// Saves the XML document to a specified file path.
            /// </summary>
            /// <param name="_Path">The file path where the XML document should be saved.</param>
            /// <remarks>
            /// Saves the document with proper formatting and UTF-8 encoding without BOM (Byte Order Mark).
            /// </remarks>
            public void SaveAt(String _Path)
            {
                using (var var_Writer = new XmlTextWriter(_Path, new UTF8Encoding(false)))
                {
                    var_Writer.Formatting = Formatting.Indented;
                    this.Save(var_Writer);
                }
            }
        }

        /// <summary>
        /// Represents an AndroidManifest.xml document and provides methods for common operations
        /// such as adding permissions or query packages.
        /// Inherits from <see cref="AndroidXmlDocument"/>.
        /// </summary>
        /// <remarks>
        /// This class simplifies the management of AndroidManifest.xml files by providing methods 
        /// to manipulate key elements, such as `<uses-permission>` and `<queries>`.
        /// </remarks>
        internal class AndroidManifest : AndroidXmlDocument
        {
            /// <summary>
            /// The root `<manifest>` element of the AndroidManifest.xml document.
            /// </summary>
            private readonly XmlElement ManifestElement;

            /// <summary>
            /// Initializes a new instance of the <see cref="AndroidManifest"/> class.
            /// </summary>
            /// <param name="_Path">The file path of the AndroidManifest.xml document to load.</param>
            /// <remarks>
            /// The constructor loads the AndroidManifest.xml file and initializes the root `<manifest>` element for further manipulation.
            /// </remarks>
            public AndroidManifest(String _Path) : base(_Path)
            {
                // Select the root <manifest> element.
                this.ManifestElement = this.SelectSingleNode("/manifest") as XmlElement;
            }

            /// <summary>
            /// Creates an Android-specific XML attribute with the specified key and value.
            /// </summary>
            /// <param name="key">The key of the Android attribute (e.g., "name").</param>
            /// <param name="value">The value of the attribute.</param>
            /// <returns>A new <see cref="XmlAttribute"/> instance representing the Android attribute.</returns>
            /// <remarks>
            /// This method simplifies the creation of namespaced attributes for the Android XML namespace.
            /// </remarks>
            private XmlAttribute CreateAndroidAttribute(String key, String value)
            {
                // Create an attribute in the Android namespace.
                XmlAttribute attr = CreateAttribute("android", key, this.AndroidXmlNamespace);
                attr.Value = value;
                return attr;
            }

            /// <summary>
            /// Adds a `<uses-permission>` element to the AndroidManifest.xml document.
            /// </summary>
            /// <param name="_Permission">The name of the permission to add (e.g., "android.permission.INTERNET").</param>
            /// <remarks>
            /// This method appends a `<uses-permission>` element with the specified permission to the root `<manifest>` element.
            /// </remarks>
            public void AddUsesPermission(String _Permission)
            {
                // Create a <uses-permission> element.
                XmlElement child = this.CreateElement("uses-permission");
                this.ManifestElement.AppendChild(child);

                // Create and append the "android:name" attribute.
                XmlAttribute newAttribute = this.CreateAndroidAttribute("name", _Permission);
                child.Attributes.Append(newAttribute);
            }

            /// <summary>
            /// Adds a `<package>` element under the `<queries>` section in the AndroidManifest.xml document.
            /// </summary>
            /// <param name="_Name">The name of the package to add (e.g., "com.example.app").</param>
            /// <remarks>
            /// This method ensures that a `<queries>` element exists in the manifest and appends a `<package>` element 
            /// with the specified package name to it.
            /// </remarks>
            public void AddQueryPackage(String _Name)
            {
                // Retrieve or create the <queries> element.
                XmlElement queryPackageElement = this.ManifestElement.SelectSingleNode("queries") as XmlElement;

                if (queryPackageElement == null)
                {
                    // If <queries> does not exist, create it.
                    queryPackageElement = this.CreateElement("queries");
                    this.ManifestElement.AppendChild(queryPackageElement);
                }

                // Create the <package> element.
                XmlElement packageElement = this.CreateElement("package");
                queryPackageElement.AppendChild(packageElement);

                // Create and append the "android:name" attribute.
                XmlAttribute newAttribute = this.CreateAndroidAttribute("name", _Name);
                packageElement.Attributes.Append(newAttribute);
            }
        }
    }
}
