using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;

namespace SingularityGroup.HotReload.Editor {
    struct BuildInfoInput {
        public readonly string allDefineSymbols;
        public readonly BuildTarget activeBuildTarget;
        public readonly string[] omittedProjects;
        public readonly bool batchMode;
        public readonly string locale;

        public BuildInfoInput(string allDefineSymbols, BuildTarget activeBuildTarget, string[] omittedProjects, bool batchMode, string locale) {
            this.allDefineSymbols = allDefineSymbols;
            this.activeBuildTarget = activeBuildTarget;
            this.omittedProjects = omittedProjects;
            this.batchMode = batchMode;
            this.locale = locale;
        }
    }
    
    static class BuildInfoHelper {
        public static async Task<BuildInfoInput> GetGenerateBuildInfoInput() {
            var buildTarget = EditorUserBuildSettings.activeBuildTarget;
            var activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines;
            var batchMode = Application.isBatchMode;
            var allDefineSymbols = await Task.Run(() => {
                return GetAllAndroidMonoBuildDefineSymbolsThreaded(activeDefineSymbols);
            });
            // cached so unexpensive most of the time
            var omittedProjects = AssemblyOmission.GetOmittedProjects(allDefineSymbols);
            var locale = PackageConst.DefaultLocale;

            return new BuildInfoInput(
                allDefineSymbols: allDefineSymbols,
                activeBuildTarget: buildTarget,
                omittedProjects: omittedProjects,
                batchMode: batchMode,
                locale: locale
            );
        }

        public static BuildInfo GenerateBuildInfoMainThread() {
            return GenerateBuildInfoMainThread(EditorUserBuildSettings.activeBuildTarget);
        }
        
        public static BuildInfo GenerateBuildInfoMainThread(BuildTarget buildTarget) {
            var allDefineSymbols = GetAllAndroidMonoBuildDefineSymbolsThreaded(EditorUserBuildSettings.activeScriptCompilationDefines);
            return GenerateBuildInfoThreaded(new BuildInfoInput(
                allDefineSymbols: allDefineSymbols, 
                activeBuildTarget: buildTarget, 
                omittedProjects: AssemblyOmission.GetOmittedProjects(allDefineSymbols),
                batchMode: Application.isBatchMode,
                locale: PackageConst.DefaultLocale
            ));
        }

        public static BuildInfo GenerateBuildInfoThreaded(BuildInfoInput input) {
            var omittedProjectRegex = String.Join("|", input.omittedProjects.Select(name => Regex.Escape(name)));
            var shortCommitHash = GitUtil.GetShortCommitHashOrFallback();
            var hostname = IsHumanControllingUs(input.batchMode) ? IpHelper.GetIpAddress() : null; 
            
            //  Note: add a string to uniquely identify the Unity project. Could use filepath to /MyProject/Assets/ (editor Application.dataPath)
            //  or application identifier (com.company.appname).
            //  Do this when supporting multiple projects: SG-28807
            //  The matching code is in Runtime assembly which compares server response with built BuildInfo.
            return new BuildInfo {
                projectIdentifier = "SG-29580",
                commitHash = shortCommitHash,
                defineSymbols = input.allDefineSymbols, 
                projectOmissionRegex = omittedProjectRegex,
                buildMachineHostName = hostname,
                buildMachinePort = RequestHelper.port,
                activeBuildTarget = input.activeBuildTarget.ToString(),
                buildMachineRequestOrigin = RequestHelper.origin,
                locale = input.locale
            };
        }

        public static bool IsHumanControllingUs(bool batchMode) {
            if (batchMode) {
                return false;
            }

            var isCI = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
            return !isCI;
        }

        private static readonly string[] editorSymbolsToRemove = {
            "PLATFORM_ARCH_64",
            "UNITY_64",
            "UNITY_INCLUDE_TESTS",
            "UNITY_EDITOR",
            "UNITY_EDITOR_64",
            "UNITY_EDITOR_WIN",
            "ENABLE_UNITY_COLLECTIONS_CHECKS",
            "ENABLE_BURST_AOT",
            "RENDER_SOFTWARE_CURSOR",
            "PLATFORM_STANDALONE_WIN",
            "PLATFORM_STANDALONE",
            "UNITY_STANDALONE_WIN",
            "UNITY_STANDALONE",
            "ENABLE_MOVIES",
            "ENABLE_OUT_OF_PROCESS_CRASH_HANDLER",
            "ENABLE_WEBSOCKET_HOST",
            "ENABLE_CLUSTER_SYNC",
            "ENABLE_CLUSTERINPUT",
        };

        private static readonly string[] androidSymbolsToAdd = { 
            "CSHARP_7_OR_LATER",
            "CSHARP_7_3_OR_NEWER",
            "PLATFORM_ANDROID",
            "UNITY_ANDROID",
            "UNITY_ANDROID_API",
            "ENABLE_EGL",
            "DEVELOPMENT_BUILD",
            "ENABLE_CLOUD_SERVICES_NATIVE_CRASH_REPORTING",
            "PLATFORM_SUPPORTS_ADS_ID",
            "UNITY_CAN_SHOW_SPLASH_SCREEN",
            "UNITY_HAS_GOOGLEVR",
            "UNITY_HAS_TANGO",
            "ENABLE_SPATIALTRACKING",
            "ENABLE_RUNTIME_PERMISSIONS",
            "ENABLE_ENGINE_CODE_STRIPPING",
            "UNITY_ASTC_ONLY_DECOMPRESS",
            "ANDROID_USE_SWAPPY",
            "ENABLE_ONSCREEN_KEYBOARD",
            "ENABLE_UNITYADS_RUNTIME",
            "UNITY_UNITYADS_API",
        };
        
        // Currently there is no better way. Alternatively we could hook into unity's call to csc.exe and parse the /define: arguments. 
        //   Hardcoding the differences was less effort and is less error prone.
        // I also looked into it and tried all the Build interfaces like this one https://docs.unity3d.com/ScriptReference/Build.IPostBuildPlayerScriptDLLs.html
        //   and logging EditorUserBuildSettings.activeScriptCompilationDefines in the callbacks - result: all same like editor, so I agree that hardcode is best. 
        private static string GetAllAndroidMonoBuildDefineSymbolsThreaded(string[] defineSymbols) {
            var defines = new HashSet<string>(defineSymbols);
            defines.ExceptWith(editorSymbolsToRemove);
            defines.UnionWith(androidSymbolsToAdd);
            // sort for consistency, must be deterministic
            var definesArray = defines.OrderBy(def => def).ToArray();
            return String.Join(";", definesArray);
        }
    }
}