﻿using System.Text.RegularExpressions;
using UnityEngine;
using UnityEditor;
using System.IO;
using System;
using System.Xml;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Debug = System.Diagnostics.Debug;
using AppodealAds.Unity.Editor.Utils;


namespace AppodealAds.Unity.Editor.Checkers
{
    [SuppressMessage("ReSharper", "InconsistentNaming")]
    public class MultidexActivator : CheckingStep
    {
        #region Constants

         //Templates in Unity Editor Data folder
        private const string gradleDefaultTemplatePath = "PlaybackEngines/AndroidPlayer/Tools/GradleTemplates";
        public const string manifestDefaultTemplatePath = "PlaybackEngines/AndroidPlayer/Apk/AndroidManifest.xml";

        //Paths without leading Assets folder
        public const string androidPluginsPath = "Plugins/Android";
        public const string gradleTemplateName = "mainTemplate.gradle";
        public const string manifestTemplateName = "AndroidManifest.xml";
        public const string appodealTemplatesPath = "Appodeal/InternalResources";
        private const string appodealDexesPath = "Assets/Plugins/Android/appodeal/assets/dex";

        //Gradle search lines
        public const string GRADLE_GOOGLE_REPOSITORY = "google()";
        public const string GRADLE_GOOGLE_REPOSITORY_COMPAT = "maven { url \"https://maven.google.com\" }";
        public const string GRADLE_DEPENDENCIES = "**DEPS**";
        public const string GRADLE_APP_ID = "**APPLICATIONID**";
        public const string GRADLE_USE_PROGUARD = "useProguard";
        public const string GRADLE_MULTIDEX_DEPENDENCY_WO_VERSION = "androidx.multidex:multidex:";
        public const string GRAFLE_DEFAULT_CONFIG = "defaultConfig";
        public const string COMPILE_OPTIONS = "compileOptions {";
        public const string GRADLE_JAVA_VERSION_1_8 = "JavaVersion.VERSION_1_8";
        public const string GRADLE_SOURCE_CAPABILITY = "sourceCompatibility ";
        public const string GRADLE_TARGET_CAPATILITY = "targetCompatibility ";

        //Gradle add lines
        public const string GRADLE_COMPILE = "compile ";
        public const string GRADLE_IMPLEMENTATION = "implementation ";
        public const string GRADLE_MULTIDEX_DEPENDENCY = "'androidx.multidex:multidex:2.0.1'";
        public const string GRADLE_MULTIDEX_ENABLE = "multiDexEnabled true";

        //Manifest add lines
        public const string manifestMutlidexApp = "androidx.multidex.MultiDexApplication";

        #endregion

        public override string getName()
        {
            return "Android Multidex Settings";
        }

        public override bool isRequiredForPlatform(BuildTarget target)
        {
            return target == BuildTarget.Android;
        }

        public override List<FixProblemInstruction> check()
        {
            var instructions = new List<FixProblemInstruction>();
            if (isNodexBuild() && EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
            {
                if (!AppodealUnityUtils.isGradleEnabled())
                {
                    if (!AppodealUnityUtils.isGradleAvailable())
                    {
                        var fix = new FixProblemInstruction(
                            "Gradle should be enabled if you use nodex version of Appodeal Plugin. " +
                            "But your version of Unity doesn't support gradle build system.Please update your Unity or use export to Eclipse or Android Studio.",
                            false);
                        instructions.Add(fix);
                        return instructions;
                    }

                    instructions.Add(new EnableGradle());
                }

                var customGradeScript = getCustomGradleScriptPath();
                if (string.IsNullOrEmpty(customGradeScript) || !File.Exists(customGradeScript))
                {
                    if (File.Exists(getDefaultGradleTemplate()))
                    {
                        instructions.Add(new CopyGradleScriptAndEnableMultidex());
                    }
                    else
                    {
                        var instr = new FixProblemInstruction(
                            "There is no build.gradle template in your Unity. " +
                            "Please ensure that your copy of Unity isn't crashed and contact Appodeal Support team.",
                            false);
                        instructions.Add(instr);
                        return instructions;
                    }
                }
                else
                {
                    var settings = new ImportantGradleSettings(customGradeScript);
                    if (!settings.isMultiDexAddedCompletely())
                    {
                        instructions.Add(new EnableMultidexInGradle(customGradeScript));
                    }

                    if (!settings.isJavaVersionIncluded())
                    {
                        instructions.Add(new EnableJavaVersion(customGradeScript));
                    }
                }

                var customManifestPath = getCustomManifestPath();
                if (string.IsNullOrEmpty(customManifestPath) || !File.Exists(customManifestPath))
                {
                    instructions.Add(new CopyManifestTemplateAndAddNameAttribute());
                }
                else
                {
                    var appNode = getApplicationNode(customManifestPath);
                    var ns = appNode.GetNamespaceOfPrefix("android");
                    if (!appNode.HasAttribute("name", ns))
                    {
                        instructions.Add(new AddNameAttrubute(appNode, ns));
                    }
                    else
                    {
                        if (appNode.GetAttribute("name", ns) == manifestMutlidexApp) return instructions;
                        var fix = new FixProblemInstruction(
                            "We found that you use custom Application class in this project. " +
                            "Please ensure that your application class meets the multidex requirements (see the official android multidex documentation).",
                            false);
                        instructions.Add(fix);
                    }
                }
            }
            else if (!isNodexBuild() && isMultidexEnabledSimpleCheck() &&
                     EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android)
            {
                var fix = new FixProblemInstruction(
                    "We found that multidex is enabled in your project. To avoid possible problems please " +
                    "replace current version of Appodeal plugin with nodex version.", false);
                instructions.Add(fix);
            }

            return instructions;
        }

        public static XmlElement getApplicationNode(string manifestPath)
        {
            var doc = new XmlDocument();
            doc.Load(manifestPath);
            var manNode = AppodealUnityUtils.XmlFindChildNode(doc, "manifest");
            var appNode = (XmlElement) AppodealUnityUtils.XmlFindChildNode(manNode, "application");
            return appNode;
        }

        private static string getCustomGradleScriptPath()
        {
            var androidDirectory = new DirectoryInfo(Path.Combine("Assets", androidPluginsPath));
            var filePaths = androidDirectory.GetFiles("*.gradle");
            return filePaths.Length > 0
                ? Path.Combine(Path.Combine(Application.dataPath, androidPluginsPath), filePaths[0].Name)
                : null;
        }

        public static string getCustomManifestPath()
        {
            return AppodealUnityUtils.combinePaths(Application.dataPath, androidPluginsPath, manifestTemplateName);
        }

        private static bool isNodexBuild()
        {
            var dexesDirectory = new DirectoryInfo(appodealDexesPath);
            if (!dexesDirectory.Exists)
            {
                return true;
            }

            var dexes = dexesDirectory.GetFiles("*.dex");
            return dexes.Length == 0;
        }

        private static bool isMultidexEnabledSimpleCheck()
        {
            var path = getCustomGradleScriptPath();
            if (string.IsNullOrEmpty(path))
            {
                return false;
            }

            var settings = new ImportantGradleSettings(path);
            return settings.isMultidexEnabled() && AppodealUnityUtils.isGradleEnabled();
        }

        public static string getDefaultGradleTemplate()
        {
            var defaultGradleTemplateFullName = AppodealUnityUtils.combinePaths(
                EditorApplication.applicationContentsPath,
                gradleDefaultTemplatePath,
                gradleTemplateName);
            if (File.Exists(defaultGradleTemplateFullName)) return defaultGradleTemplateFullName;
            var unixAppContentsPath =
                Path.GetDirectoryName(Path.GetDirectoryName(EditorApplication.applicationContentsPath));
            defaultGradleTemplateFullName = AppodealUnityUtils.combinePaths(unixAppContentsPath,
                gradleDefaultTemplatePath,
                gradleTemplateName);

            return defaultGradleTemplateFullName;
        }
    }

    [SuppressMessage("ReSharper", "InconsistentNaming")]
    [SuppressMessage("ReSharper", "NotAccessedField.Local")]
    public class ImportantGradleSettings
    {
        public readonly bool googleRepositoryPresented;
        public readonly bool multidexDependencyPresented;
        public readonly bool multidexEnabled;
        public readonly bool deprecatedProguardPresented;
        public readonly bool compileOptions;
        public readonly bool sourceCapability;
        public readonly bool targetCapability;
        public bool defaultConfig;

        public ImportantGradleSettings(string gradlePath)
        {
            var gradleScriptFullText = File.ReadAllText(gradlePath);
            multidexDependencyPresented =
                gradleScriptFullText.Contains(MultidexActivator.GRADLE_MULTIDEX_DEPENDENCY_WO_VERSION);
            multidexEnabled = gradleScriptFullText.Contains(MultidexActivator.GRADLE_MULTIDEX_ENABLE);
            deprecatedProguardPresented = gradleScriptFullText.Contains(MultidexActivator.GRADLE_USE_PROGUARD);
            compileOptions = gradleScriptFullText.Contains(MultidexActivator.COMPILE_OPTIONS);
            sourceCapability =
                gradleScriptFullText.Contains(MultidexActivator.GRADLE_SOURCE_CAPABILITY +
                                              MultidexActivator.GRADLE_JAVA_VERSION_1_8);
            targetCapability =
                gradleScriptFullText.Contains(MultidexActivator.GRADLE_TARGET_CAPATILITY +
                                              MultidexActivator.GRADLE_JAVA_VERSION_1_8);
            defaultConfig = gradleScriptFullText.Contains(MultidexActivator.GRAFLE_DEFAULT_CONFIG);

            var allprojects = getModule("allprojects", gradleScriptFullText);
            googleRepositoryPresented = allprojects.Contains(MultidexActivator.GRADLE_GOOGLE_REPOSITORY) ||
                                        gradleScriptFullText.Contains(MultidexActivator
                                            .GRADLE_GOOGLE_REPOSITORY_COMPAT);
        }

        public bool isJavaVersionIncluded()
        {
            return sourceCapability && targetCapability;
        }

        public bool isMultidexEnabled()
        {
            return multidexDependencyPresented || multidexEnabled;
        }

        public bool isMultiDexAddedCompletely()
        {
            return multidexDependencyPresented && multidexEnabled && !deprecatedProguardPresented &&
                   googleRepositoryPresented;
        }

        private static string getModule(string moduleName, string fulltext)
        {
            var startIndex = fulltext.IndexOf(moduleName, StringComparison.Ordinal);
            startIndex = fulltext.IndexOf('{', startIndex) + 1;
            var currentIndex = startIndex;
            var braces = 1;
            while (braces != 0)
            {
                var c = fulltext[currentIndex];
                switch (c)
                {
                    case '{':
                        braces++;
                        break;
                    case '}':
                        braces--;
                        break;
                }

                currentIndex++;
            }

            return fulltext.Substring(startIndex, currentIndex - startIndex);
        }
    }

    internal class EnableGradle : FixProblemInstruction
    {
        public EnableGradle() : base(
            "Gradle build system isn't enabled.\n(required for nodex version of Appodeal Plugin " +
            "and if you aren't going to export your project to Android Studio or Eclipse).", true)
        {
        }

        public override void fixProblem()
        {
            AppodealUnityUtils.enableGradleBuildSystem();
        }
    }

    internal class CopyGradleScriptAndEnableMultidex : FixProblemInstruction
    {
        public CopyGradleScriptAndEnableMultidex() : base("Assets/Plugins/Android/mainTemplate.gradle not found.\n" +
                                                          "(required if you aren't going to export your project to Android Studio or Eclipse)",
            true)
        {
        }

        public override void fixProblem()
        {
            // EditorApplication.applicationContentsPath is different for macos and win. need to fix to reach manifest and gradle templates
            var defaultGradleTemplateFullName = MultidexActivator.getDefaultGradleTemplate();

            var destGradleScriptFullName = AppodealUnityUtils.combinePaths(Application.dataPath,
                MultidexActivator.androidPluginsPath,
                MultidexActivator.gradleTemplateName);
            //Prefer to use build.gradle from the box. Just in case.
            if (File.Exists(defaultGradleTemplateFullName))
            {
                File.Copy(defaultGradleTemplateFullName, destGradleScriptFullName);
            }

            AssetDatabase.ImportAsset(AppodealUnityUtils.absolute2Relative(destGradleScriptFullName),
                ImportAssetOptions.ForceUpdate);

            //There are no multidex settings in default build.gradle but they can add that stuff.
            var settings = new ImportantGradleSettings(destGradleScriptFullName);

            if (!settings.isMultiDexAddedCompletely())
                new EnableMultidexInGradle(destGradleScriptFullName).fixProblem();
        }
    }

    internal class EnableMultidexInGradle : FixProblemInstruction
    {
        private readonly string path;

        public EnableMultidexInGradle(string gradleScriptPath) : base(
            "Multidex isn't enabled. mainTemplate.gradle should be edited " +
            "according to the official documentation:\nhttps://developer.android.com/studio/build/multidex", true)
        {
            path = gradleScriptPath;
        }

        public override void fixProblem()
        {
            var settings = new ImportantGradleSettings(path);
            var leadingWhitespaces = "";
            string line;
            var prevLine = "";
            var modifiedGradle = "";
            var gradleScript = new StreamReader(path);
            string multidexDependency;
            var comparsionUnityVersionWith20182 =
                AppodealUnityUtils.compareVersions(Application.unityVersion, "2018.2");
            if (comparsionUnityVersionWith20182 < 0)
            {
                multidexDependency = MultidexActivator.GRADLE_COMPILE + MultidexActivator.GRADLE_MULTIDEX_DEPENDENCY;
            }
            else
            {
                multidexDependency = MultidexActivator.GRADLE_IMPLEMENTATION +
                                     MultidexActivator.GRADLE_MULTIDEX_DEPENDENCY;
            }

            while ((line = gradleScript.ReadLine()) != null)
            {
                if (!settings.multidexDependencyPresented && line.Contains(MultidexActivator.GRADLE_DEPENDENCIES))
                {
                    modifiedGradle += leadingWhitespaces + multidexDependency + Environment.NewLine;
                }

                if (!settings.multidexEnabled && line.Contains(MultidexActivator.GRADLE_APP_ID))
                {
                    modifiedGradle += leadingWhitespaces + MultidexActivator.GRADLE_MULTIDEX_ENABLE +
                                      Environment.NewLine;
                }

                if (settings.deprecatedProguardPresented && line.Contains(MultidexActivator.GRADLE_USE_PROGUARD))
                {
                    continue;
                }

                modifiedGradle += line + Environment.NewLine;
                leadingWhitespaces = Regex.Match(line, "^\\s*").Value;
                if (line.Contains("repositories") && prevLine.Contains("allprojects") &&
                    !settings.googleRepositoryPresented)
                {
                    leadingWhitespaces += leadingWhitespaces;
                    modifiedGradle += leadingWhitespaces + MultidexActivator.GRADLE_GOOGLE_REPOSITORY_COMPAT +
                                      Environment.NewLine;
                }

                prevLine = line;
            }

            gradleScript.Close();
            File.WriteAllText(path, modifiedGradle);
            AssetDatabase.ImportAsset(AppodealUnityUtils.absolute2Relative(path), ImportAssetOptions.ForceUpdate);
        }
    }

    internal class EnableJavaVersion : FixProblemInstruction
    {
        private readonly string path;

        public EnableJavaVersion(string gradleScriptPath) : base("Java version isn't included to mainTemplate.gradle",
            true)
        {
            path = gradleScriptPath;
        }

        public override void fixProblem()
        {
            var settings = new ImportantGradleSettings(path);
            var leadingWhitespaces = "    ";
            const string additionalWhiteSpaces = "";
            string line;
            var modifiedGradle = "";

            var gradleScript = new StreamReader(path);

            while ((line = gradleScript.ReadLine()) != null)
            {
                if (line.Contains(MultidexActivator.GRAFLE_DEFAULT_CONFIG))
                {
                    if (!settings.compileOptions)
                    {
                        modifiedGradle += additionalWhiteSpaces + leadingWhitespaces +
                                          MultidexActivator.COMPILE_OPTIONS + Environment.NewLine;
                    }

                    if (!settings.sourceCapability)
                    {
                        modifiedGradle += leadingWhitespaces + leadingWhitespaces +
                                          MultidexActivator.GRADLE_SOURCE_CAPABILITY
                                          + MultidexActivator.GRADLE_JAVA_VERSION_1_8 + Environment.NewLine;
                    }

                    if (!settings.targetCapability)
                    {
                        modifiedGradle += leadingWhitespaces + leadingWhitespaces +
                                          MultidexActivator.GRADLE_TARGET_CAPATILITY
                                          + MultidexActivator.GRADLE_JAVA_VERSION_1_8 + Environment.NewLine;
                    }

                    if (!settings.targetCapability)
                    {
                        modifiedGradle += leadingWhitespaces + "}" + Environment.NewLine;
                    }

                    if (!settings.targetCapability)
                    {
                        modifiedGradle += leadingWhitespaces + Environment.NewLine;
                    }
                }

                modifiedGradle += line + Environment.NewLine;
                leadingWhitespaces = Regex.Match(line, "^\\s*").Value;
            }

            gradleScript.Close();
            File.WriteAllText(path, modifiedGradle);
            AssetDatabase.ImportAsset(AppodealUnityUtils.absolute2Relative(path), ImportAssetOptions.ForceUpdate);
        }
    }

    internal class CopyManifestTemplateAndAddNameAttribute : FixProblemInstruction
    {
        public CopyManifestTemplateAndAddNameAttribute() : base(
            "Assets/Plugins/Android/AndroidManifest.xml not found.\n", true)
        {
        }

        public override void fixProblem()
        {
            var fullManifestName = MultidexActivator.getCustomManifestPath();
            var defaultTemplate = Path.Combine(EditorApplication.applicationContentsPath,
                MultidexActivator.manifestDefaultTemplatePath);
            if (!File.Exists(defaultTemplate))
            {
                var unixAppContentsPath =
                    Path.GetDirectoryName(Path.GetDirectoryName(EditorApplication.applicationContentsPath));
                Debug.Assert(unixAppContentsPath != null, nameof(unixAppContentsPath) + " != null");
                defaultTemplate = Path.Combine(unixAppContentsPath, MultidexActivator.manifestDefaultTemplatePath);
            }

            var appodealTemplate = AppodealUnityUtils.combinePaths(Application.dataPath,
                MultidexActivator.appodealTemplatesPath,
                MultidexActivator.manifestTemplateName);
            File.Copy(File.Exists(defaultTemplate) ? defaultTemplate : appodealTemplate, fullManifestName);
            AssetDatabase.ImportAsset(AppodealUnityUtils.absolute2Relative(fullManifestName),
                ImportAssetOptions.ForceUpdate);

            var appNode = MultidexActivator.getApplicationNode(fullManifestName);
            var ns = appNode.GetNamespaceOfPrefix("android");
            if (!appNode.HasAttribute("name", ns))
            {
                new AddNameAttrubute(appNode, ns).fixProblem();
            }
        }
    }

    internal class AddNameAttrubute : FixProblemInstruction
    {
        private readonly XmlElement appNode;
        private readonly string ns;

        public AddNameAttrubute(XmlElement appNode, string ns) : base(
            "Problem in AndroidManifest.xml found.\nApplication tag should contain android:" +
            "name attribute with value: " + MultidexActivator.manifestMutlidexApp, true)
        {
            this.appNode = appNode;
            this.ns = ns;
        }

        public override void fixProblem()
        {
            var fullManifestName = MultidexActivator.getCustomManifestPath();
            appNode.SetAttribute("name", ns, MultidexActivator.manifestMutlidexApp);
            Debug.Assert(appNode.OwnerDocument != null, "appNode.OwnerDocument != null");
            appNode.OwnerDocument.Save(fullManifestName);
            AssetDatabase.ImportAsset(AppodealUnityUtils.absolute2Relative(fullManifestName),
                ImportAssetOptions.ForceUpdate);
        }
    }
}