using UnityEngine;
using System.IO;
using System;
using System.Xml;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor.Android;

namespace Combo
{
    public class ComboSDKAndroidPostBuild : IPostGenerateGradleAndroidProject 
    {
        public int callbackOrder
        {
            get { return 0; }
        }
        ProjectPath path;

        public void OnPostGenerateGradleAndroidProject(string exportPath)
        {
            exportPath = Path.GetDirectoryName(exportPath);
            Log($"Start post build for Android: {exportPath}");
            path = new ProjectPath(exportPath);
            UpdateAndroidManifest();
            UpdateGradle();
        }

        void UpdateAndroidManifest()
        {
            // Set activity launchMode to singleTop
            var libraryManifest = AndroidManifest.Load(path.LibraryManifestPath)
                .SetLaunchMode("singleTop")
                .GetActivityName(out string activityName);
            Log($"Set main activity launchMode to \"singleTop\"\n  -> {path.LibraryManifestPath}");

            libraryManifest.GetApplicationName(out string applicationName);
            if (applicationName != null) {
                LogError($"Defining application android::name in UnityLibraryManifest is not allowed, potentially causing manifest merge conflicts.");
                libraryManifest.RemoveApplicationName();
                LogWarning($"Define in application android::name in UnityLibraryManifest is removed.");
            }

            libraryManifest.GetApplicationIcon(out string applicationIcon);
            if (applicationIcon != null) {
                LogError($"Defining application android::icon in UnityLibraryManifest is not allowed, potentially causing manifest merge conflicts.");
                libraryManifest.RemoveApplicationIcon();
                LogWarning($"Define in application android::icon in UnityLibraryManifest is removed.");
            }

            // Replace activity to ComboSDKMainActivity if not using custom activity
            if (activityName == "com.unity3d.player.UnityPlayerActivity") {
                libraryManifest.SetActivityName("com.seayoo.sdk.ComboSDKMainActivity");
                Log($"Find default activity \"{activityName}\", replace with \"com.seayoo.sdk.ComboSDKMainActivity\"\n  -> {path.LibraryManifestPath}");
            } else {
                LogWarning($"Find custom activity \"{activityName}\", skip activity replacing\n  -> {path.LibraryManifestPath}");
            }

            libraryManifest.Save();
            
            // Set default application to ComboSDKApplication, set icon to combosdk_ic_launcher
            var launcherManifest = AndroidManifest
                .Load(path.LauncherManifestPath);

            launcherManifest.GetApplicationName(out applicationName);
            if (applicationName != null) {
                LogWarning($"Find custom application \"{applicationName}\", skip application replacing\n  -> {path.LauncherManifestPath}");
            } else {
                launcherManifest.SetApplicationName("com.seayoo.sdk.ComboSDKApplication");
                Log($"Application set to \"com.seayoo.sdk.ComboSDKApplication\"\n  -> {path.LauncherManifestPath}");
            }
            
            launcherManifest.SetApplicationIcon("@mipmap/combosdk_ic_launcher")
                .SetAllowBackup("false")
                .AppendToolsReplace("android:allowBackup")
                .AppendToolsReplace("android:icon")
                .Save();
            
            Log($"Application icon set to \"@mipmap/combosdk_ic_launcher\"\n  -> {path.LauncherManifestPath}");
        }
#if UNITY_2022_1_OR_NEWER
        void UpdateGradle() {
            Gradle.Load(path.LauncherGradlePath)
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDK.gradle\")")
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDKSigning.gradle\")")
                .SelectLine(s => s.Contains("compileSdkVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+)", "33"))
                .Save();
            Log($"Append \"ComboSDK.gradle\"\n  -> {path.LauncherGradlePath}");

            Gradle.Load(path.LibraryGradlePath)
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDKDeps.gradle\")")
                .SelectLine(s => s.Contains("compileSdkVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+)", "33"))
                .Save();
            Log($"Append \"ComboSDKDeps.gradle\"\n  -> {path.LibraryGradlePath}");

            // Update gradle properties
            Gradle.Load(path.GradlePropsPath)
                .RemoveLine(s => s.Contains("android.useAndroidX"))
                .AppendLine("android.useAndroidX=true")
                .RemoveLine(s => s.Contains("android.enableJetifier"))
                .AppendLine("android.enableJetifier=true")
                .Save();
            Log($"Enable \"android.useAndroidX\"\n  -> {path.GradlePropsPath}");
            Log($"Enable \"android.enableJetifier\"\n  -> {path.GradlePropsPath}");
        }
#else
        void UpdateGradle() {
            // Append ComboSDK.gradle to /launcher/build.gradle, set buildToolsVersion to 30.0.2
            Gradle.Load(path.LauncherGradlePath)
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDK.gradle\")")
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDKSigning.gradle\")")
                .SelectLine(s => s.Contains("buildToolsVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+\.\d+\.\d+)", "30.0.2"))
                .SelectLine(s => s.Contains("compileSdkVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+)", "33"))
                .Save();
            Log($"Append \"ComboSDK.gradle\"\n  -> {path.LauncherGradlePath}");
            Log($"Set \"buildToolsVersion\" to \"30.0.2\"\n  -> {path.LauncherGradlePath}");

            // Append ComboSDKDeps.gradle to /unityLibrary/build.gradle, set buildToolsVersion to 30.0.2
            Gradle.Load(path.LibraryGradlePath)
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDKDeps.gradle\")")
                .SelectLine(s => s.Contains("buildToolsVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+\.\d+\.\d+)", "30.0.2"))
                .SelectLine(s => s.Contains("compileSdkVersion"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+)", "33"))
                .Save();
            Log($"Append \"ComboSDKDeps.gradle\"\n  -> {path.LibraryGradlePath}");
            Log($"Set \"buildToolsVersion\" to \"30.0.2\"\n  -> {path.LibraryGradlePath}");

            // Append ComboSDKRepo.gradle to /build.gradle, set com.android.tools.build:gradle to 4.2.2
            Gradle.Load(path.RootGradlePath)
                .AppendLine("apply from: (\"${rootProject.rootDir}/ComboSDKRepo.gradle\")")
                .SelectLine(s => s.Contains("com.android.tools.build:gradle"))
                .UpdateLine(s => Regex.Replace(s, @"(\d+\.\d+\.\d+)", "4.2.2"))
                .Save();
            Log($"Append \"ComboSDKRepo.gradle\"\n  -> {path.RootGradlePath}");
            Log($"Set \"com.android.tools.build:gradle\" to \"4.2.2\"\n  -> {path.RootGradlePath}");

            // Update gradle properties
            Gradle.Load(path.GradlePropsPath)
                .RemoveLine(s => s.Contains("android.useAndroidX"))
                .AppendLine("android.useAndroidX=true")
                .RemoveLine(s => s.Contains("android.enableJetifier"))
                .AppendLine("android.enableJetifier=true")
                .Save();
            Log($"Enable \"android.useAndroidX\"\n  -> {path.GradlePropsPath}");
            Log($"Enable \"android.enableJetifier\"\n  -> {path.GradlePropsPath}");
        }
#endif

        internal class AndroidManifest
        {
            string srcPath;
            XmlDocument xml;
            XmlNode ctx = null;
            const string androidNamespaceURI = "http://schemas.android.com/apk/res/android";
            const string toolsNamespaceURI = "http://schemas.android.com/tools";
            XmlNamespaceManager namespaceManager;

            internal static AndroidManifest Load(string path)
            {
                var manifest = new AndroidManifest
                {
                    xml = new XmlDocument()
                };
                manifest.xml.Load(path);
                manifest.namespaceManager = new XmlNamespaceManager(manifest.xml.NameTable);
                manifest.namespaceManager.AddNamespace("android", androidNamespaceURI);
                manifest.namespaceManager.AddNamespace("tools", toolsNamespaceURI);
                manifest.srcPath = path;
                return manifest;
            }

            internal AndroidManifest Save(string path = null)
            {
                if (string.IsNullOrEmpty(path))
                    path = srcPath;
                xml.Save(path);
                return this;
            }

            internal AndroidManifest SetLaunchMode(string value) => Select("//action[@android:name='android.intent.action.MAIN']/../..").SetAttr("android:launchMode", value);
            internal AndroidManifest SetActivityName(string value) => Select("//action[@android:name='android.intent.action.MAIN']/../..").SetAttr("android:name", value);
            internal AndroidManifest GetActivityName(out string value) => Select("//action[@android:name='android.intent.action.MAIN']/../..").GetAttr("android:name", out value);
            internal AndroidManifest GetApplicationName(out string value) => Select("/manifest/application").GetAttr("android:name", out value);
            internal AndroidManifest RemoveApplicationName() => Select("/manifest/application").RemoveAttr("android:name");
            internal AndroidManifest SetApplicationName(string value) => Select("/manifest/application").SetAttr("android:name", value);
            internal AndroidManifest SetApplicationIcon(string value) => Select("/manifest/application").SetAttr("android:icon", value);
            internal AndroidManifest GetApplicationIcon(out string value) => Select("/manifest/application").GetAttr("android:icon", out value);
            internal AndroidManifest RemoveApplicationIcon() => Select("/manifest/application").RemoveAttr("android:icon");
            internal AndroidManifest SetAllowBackup(string value) => Select("/manifest/application").SetAttr("android:allowBackup", value);
            internal AndroidManifest AppendToolsReplace(string value) {
                Select("/manifest/application").GetAttr("tools:replace", out string toolReplace);
                if (toolReplace == null) {
                    toolReplace = value;
                } else if (!toolReplace.Contains(value)) {
                    toolReplace += $",{value}";
                }
                SetAttr("tools:replace", toolReplace);
                return this;
            }
            
            AndroidManifest Select(string value)
            {
                ctx = xml.SelectSingleNode(value, namespaceManager);
                Assert(ctx != null, $"Cannot find {value} in ${srcPath}");
                return this;
            }

            AndroidManifest SetAttr(string attributeName, string value)
            {
                Assert(ctx != null, $"SetAttr {attributeName} failed, context is null");
                XmlAttribute attribute = ctx.Attributes[attributeName];
                if (attribute != null)
                {
                    attribute.Value = value;
                }
                else
                {
                    XmlAttribute newAttribute;
                    if (attributeName.StartsWith("android:"))
                        newAttribute = xml.CreateAttribute(attributeName, androidNamespaceURI);
                    else if (attributeName.StartsWith("tools:"))
                        newAttribute = xml.CreateAttribute(attributeName, toolsNamespaceURI);
                    else
                        newAttribute = xml.CreateAttribute(attributeName);
                    newAttribute.Value = value;
                    ctx.Attributes.Append(newAttribute);
                }

                return this;
            }

            AndroidManifest GetAttr(string attributeName, out string value)
            {
                Assert(ctx != null, $"GetAttr {attributeName} failed, context is null");
                XmlAttribute attribute = ctx.Attributes[attributeName];
                if (attribute != null) value = attribute.Value;
                else value = null;
                return this;
            }
            public AndroidManifest RemoveAttr(string attributeName)
            {
                Assert(ctx != null, $"RemoveAttr {attributeName} failed, context is null");
                XmlAttribute attribute = ctx.Attributes[attributeName];
                if (attribute != null) ctx.Attributes.Remove(attribute);
                return this;
            }
        }

        internal class Gradle
        {
            string srcPath;
            int ctx = 0;
            List<string> content;

            internal static Gradle Load(string path)
            {
                return new Gradle
                {
                    content = new List<string>(File.ReadAllLines(path)),
                    srcPath = path
                };
            }
            internal Gradle Save(string path = null) {
                if (string.IsNullOrEmpty(path))
                    path = srcPath;
                File.WriteAllLines(path, content);
                return this;
            }
            internal Gradle AppendLine(string value)
            {
                content.Add(value);
                return this;
            }
            internal Gradle UpdateLine(Func<string, string> func) {
                GetContext(out string line);
                var newLine = func(line);
                content[ctx] = newLine;
                return this;
            }
            internal Gradle SelectLine(Predicate<string> predicate)
            {
                ctx = content.FindIndex(predicate);
                Assert(ctx != -1, $"Failed to select line {predicate} in {srcPath}");
                return this;
            }
            internal Gradle RemoveLine(Predicate<string> predicate)
            {
                var idx = content.FindIndex(predicate);
                if (idx != -1) {
                    content.RemoveAt(idx);
                }
                return this;
            }
            Gradle GetContext(out string value) {
                value = content[ctx];
                return this;
            }
        }

        internal class ProjectPath
        {
            internal string RootPath { get; }

            internal ProjectPath(string path)
            {
                RootPath = path;
            }

            internal string LauncherSourcePath
            {
                get { return Path.Combine(RootPath, "launcher/src/main"); }
            }
            internal string LibrarySourcePath
            {
                get 
                    {
#if TUANJIE_1
                       return Path.Combine(RootPath, "tuanjieLibrary/src/main");
#else
                       return Path.Combine(RootPath, "unityLibrary/src/main");
#endif
                    }
            }
            internal string LauncherManifestPath
            {
                get { return Path.Combine(LauncherSourcePath, "AndroidManifest.xml"); }
            }
            internal string LibraryManifestPath
            {
                get { return Path.Combine(LibrarySourcePath, "AndroidManifest.xml"); }
            }
            internal string LauncherGradlePath
            {
                get { return Path.Combine(RootPath, "launcher/build.gradle"); }
            }
            internal string LibraryGradlePath
            {
                get 
                    {
#if TUANJIE_1
                      return Path.Combine(RootPath, "tuanjieLibrary/build.gradle");
#else
                      return Path.Combine(RootPath, "unityLibrary/build.gradle");
#endif
                    }
            }
            internal string RootGradlePath
            {
                get { return Path.Combine(RootPath, "build.gradle"); }
            }
            internal string GradlePropsPath
            {
                get { return Path.Combine(RootPath, "gradle.properties"); }
            }
        }

        static void Assert(bool condition, object message) {
            Debug.Assert(condition, message);
            if (!condition) throw new Exception($"Assert failed: {message}");
        }
        static void Log(object message) => Debug.Log($"[ComboSDK] AndroidPostBuilder: {message}");
        static void LogWarning(object message) => Debug.LogWarning($"[ComboSDK] AndroidPostBuilder: {message}");
        static void LogError(object message) => Debug.LogError($"[ComboSDK] AndroidPostBuilder: {message}");
    }
}
