#if UNITY_EDITOR

using RuntimeUnitTestToolkit;
using RuntimeUnitTestToolkit.Editor;
using System;
using UnityEditor;
using UnityEditor.Build.Reporting;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

internal class RuntimeUnitTestSettings
{
    public ScriptingImplementation ScriptBackend;
    public bool UseCurrentScriptBackend;
    public BuildTarget BuildTarget;
    public bool UseCurrentBuildTarget;

    public bool Headless;
    public bool AutoRunPlayer;
    public bool DisableAutoClose;

    public RuntimeUnitTestSettings()
    {
        UseCurrentBuildTarget = true;
        UseCurrentScriptBackend = true;
        Headless = false;
        AutoRunPlayer = true;
        DisableAutoClose = false;
    }

    public override string ToString()
    {
        return $"{ScriptBackend} {BuildTarget} Headless:{Headless} AutoRunPlayer:{AutoRunPlayer} DisableAutoClose:{DisableAutoClose}";
    }
}

// no namespace(because invoke from commandline)
public static partial class UnitTestBuilder
{
    const string SettingsKeyBase = "RuntimeUnitTest.Settings.";

    [MenuItem("Test/BuildUnitTest")]
    public static void BuildUnitTest()
    {
        var settings = new RuntimeUnitTestSettings(); // default

        string buildPath = null;

        if (Application.isBatchMode) // from commandline
        {
            settings.AutoRunPlayer = false;
            settings.DisableAutoClose = false;

            var cmdArgs = Environment.GetCommandLineArgs();
            for (int i = 0; i < cmdArgs.Length; i++)
            {
                if (string.Equals(cmdArgs[i].Trim('-', '/'), "ScriptBackend", StringComparison.OrdinalIgnoreCase))
                {
                    settings.UseCurrentScriptBackend = false;
                    var str = cmdArgs[++i];
                    if (str.StartsWith("mono", StringComparison.OrdinalIgnoreCase))
                    {
                        settings.ScriptBackend = ScriptingImplementation.Mono2x;
                    }
                    else if (str.StartsWith("IL2CPP", StringComparison.OrdinalIgnoreCase))
                    {
                        settings.ScriptBackend = ScriptingImplementation.IL2CPP;
                    }
                    else
                    {
                        settings.ScriptBackend = (ScriptingImplementation)Enum.Parse(typeof(ScriptingImplementation), str, true);
                    }
                }
                else if (string.Equals(cmdArgs[i].Trim('-', '/'), "BuildTarget", StringComparison.OrdinalIgnoreCase))
                {
                    settings.UseCurrentBuildTarget = false;
                    settings.BuildTarget = (BuildTarget)Enum.Parse(typeof(BuildTarget), cmdArgs[++i], true);
                }
                else if (string.Equals(cmdArgs[i].Trim('-', '/'), "Headless", StringComparison.OrdinalIgnoreCase))
                {
                    settings.Headless = true;
                }
                else if (string.Equals(cmdArgs[i].Trim('-', '/'), "buildPath", StringComparison.OrdinalIgnoreCase))
                {
                    buildPath = cmdArgs[++i];
                }
            }
        }
        else
        {
            var key = SettingsKeyBase + Application.productName;
            var settingsValue = EditorPrefs.GetString(key, null);
            try
            {
                if (!string.IsNullOrWhiteSpace(settingsValue))
                {
                    settings = JsonUtility.FromJson<RuntimeUnitTestSettings>(settingsValue);
                }
            }
            catch
            {
                UnityEngine.Debug.LogError("Fail to load RuntimeUnitTest settings");
                EditorPrefs.SetString(key, null);
            }
        }

        if (settings.UseCurrentBuildTarget)
        {
            settings.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
        }
        if (settings.UseCurrentScriptBackend)
        {
            settings.ScriptBackend = PlayerSettings.GetScriptingBackend(ToBuildTargetGroup(settings.BuildTarget));
        }

        if (buildPath == null)
        {
            buildPath = $"bin/UnitTest/{settings.BuildTarget}_{settings.ScriptBackend}/test" + (IsWindows(settings.BuildTarget) ? ".exe" : "");
        }

        var originalScene = SceneManager.GetActiveScene().path;

        BuildUnitTest(buildPath, settings.ScriptBackend, settings.BuildTarget, settings.Headless, settings.AutoRunPlayer, settings.DisableAutoClose);

        // reopen original scene
        if (!string.IsNullOrWhiteSpace(originalScene))
        {
            EditorSceneManager.OpenScene(originalScene, OpenSceneMode.Single);
        }
        else
        {
            EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);
        }
    }


    [MenuItem("Test/LoadUnitTestScene")]
    public static void LoadUnitTestScene()
    {
        var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
        BuildUnitTestRunnerScene();
        EditorSceneManager.MarkSceneDirty(scene);
    }

    static RuntimeUnitTestSettings LoadOrGetDefaultSettings()
    {
        var key = SettingsKeyBase + Application.productName;

        var settingsValue = EditorPrefs.GetString(key, null);
        RuntimeUnitTestSettings settings = null;
        try
        {
            if (!string.IsNullOrWhiteSpace(settingsValue))
            {
                settings = JsonUtility.FromJson<RuntimeUnitTestSettings>(settingsValue);
            }
        }
        catch
        {
            UnityEngine.Debug.LogError("Fail to load RuntimeUnitTest settings");
            EditorPrefs.SetString(key, null);
            settings = null;
        }

        if (settings == null)
        {
            // default
            settings = new RuntimeUnitTestSettings
            {
                UseCurrentBuildTarget = true,
                UseCurrentScriptBackend = true,
                Headless = false,
                AutoRunPlayer = true,
            };
        }

        return settings;
    }

    static void SaveSettings(RuntimeUnitTestSettings settings)
    {
        var key = SettingsKeyBase + Application.productName;
        EditorPrefs.SetString(key, JsonUtility.ToJson(settings));
    }

    public static void BuildUnitTest(string buildPath, ScriptingImplementation scriptBackend, BuildTarget buildTarget, bool headless, bool autoRunPlayer, bool disableAutoClose)
    {
        var sceneName = "Assets/TempRuntimeUnitTestScene_" + DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        if (disableAutoClose)
        {
            sceneName += "_DisableAutoClose";
        }
        sceneName += ".unity";

        var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);

        BuildUnitTestRunnerScene();

        EditorSceneManager.MarkSceneDirty(scene);
        AssetDatabase.SaveAssets();
        EditorSceneManager.SaveScene(scene, sceneName, false);
        try
        {
            Build(sceneName, buildPath, new RuntimeUnitTestSettings { ScriptBackend = scriptBackend, BuildTarget = buildTarget, Headless = headless, AutoRunPlayer = autoRunPlayer, DisableAutoClose = disableAutoClose });
        }
        finally
        {
            AssetDatabase.DeleteAsset(sceneName);
        }
    }

    public static UnitTestRunner BuildUnitTestRunnerScene()
    {
        const string kStandardSpritePath = "UI/Skin/UISprite.psd";
        const string kBackgroundSpritePath = "UI/Skin/Background.psd";
        var uisprite = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
        var background = AssetDatabase.GetBuiltinExtraResource<Sprite>(kBackgroundSpritePath);

        ScrollRect buttonList;
        VerticalLayoutGroup listLayout;
        Scrollbar refListScrollbar;
        ScrollRect logList;
        Scrollbar refLogScrollbar;
        Button clearButton;
        Text logText;

        // Flutter like coded build utility

        var rootObject = new Builder<Camera>("SceneRoot")
        {
            Children = new IBuilder[] {
                    new Builder<EventSystem, StandaloneInputModule>("EventSystem"),
                    new Builder<Canvas, CanvasScaler, GraphicRaycaster>("Canvas") {
                        Component1 = { renderMode = RenderMode.ScreenSpaceOverlay },
                        Children = new IBuilder[] {
                            new Builder<HorizontalLayoutGroup, CanvasRenderer>("HorizontalSplitter") {
                                RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1) },
                                Component1 = { childControlWidth = true, childControlHeight = true, spacing = 10 },
                                Children = new IBuilder[] {
                                    new Builder<ScrollRect, CanvasRenderer>("ButtonList", out buttonList) {
                                        RectTransform = { pivot = new Vector2(0.5f, 0.5f) },
                                        Component1 = { horizontal =false, vertical = true, movementType = ScrollRect.MovementType.Clamped },
                                        Children = new IBuilder[] {
                                            new Builder<VerticalLayoutGroup, ContentSizeFitter>("ListLayoutToAttach", out listLayout) {
                                                RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1), pivot = new Vector2(0, 1) },
                                                Component1  = { childControlWidth = true, childControlHeight = true, childForceExpandWidth = true, childForceExpandHeight = false, spacing = 10, padding = new RectOffset(10,20,10,10) },
                                                Component2 = { horizontalFit = ContentSizeFitter.FitMode.Unconstrained, verticalFit = ContentSizeFitter.FitMode.PreferredSize },
                                                SetTarget = self => { buttonList.content = self.GetComponent<RectTransform>();  },
                                                Child = new Builder<Button, Image, LayoutElement>("ClearButton", out clearButton) {
                                                    Component2 = { sprite = uisprite, type = Image.Type.Sliced },
                                                    Component3 = { minHeight = 50 },
                                                    SetTarget = self => { self.GetComponent<Button>().targetGraphic = self.GetComponent<Graphic>(); },
                                                    Child = new Builder<Text>("ButtonText") {
                                                        RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1), pivot = new Vector2(0.5f, 0.5f) },
                                                        Component1 = { text = "Clear", color = FromRGB(50, 50, 50), alignment = TextAnchor.MiddleCenter, fontSize = 24, lineSpacing = 1 }
                                                    }
                                                }
                                            },
                                            new Builder<Scrollbar,Image>("ListScrollbar", out refListScrollbar) {
                                                RectTransform = { anchorMin = new Vector2(1, 0), anchorMax = new Vector2(1, 1) },
                                                Component1 = { navigation = new Navigation{ mode = Navigation.Mode.None }, direction = Scrollbar.Direction.BottomToTop, size = 1.0f },
                                                Component2 = { sprite = background, type = Image.Type.Sliced },
                                                SetTarget = self => { buttonList.verticalScrollbar = self.GetComponent<Scrollbar>(); },
                                                Child = new Builder<RectTransform>("Sliding Area") {
                                                    RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1) },
                                                    Child = new Builder<Image>("Handle") {
                                                        Component1 = { sprite = uisprite, type = Image.Type.Sliced },
                                                        SetTarget = self =>
                                                        {
                                                            refListScrollbar.targetGraphic = self.GetComponent<Graphic>();
                                                            refListScrollbar.handleRect = self.GetComponent<RectTransform>();
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    },
                                    new Builder<ScrollRect, CanvasRenderer>("ScrollableText", out logList) {
                                        RectTransform = { pivot = new Vector2(0.5f, 0.5f) },
                                        Component1 = { horizontal =false, vertical = true, movementType = ScrollRect.MovementType.Elastic, elasticity = 0.1f },
                                        Children = new IBuilder[] {
                                            new Builder<Text, ContentSizeFitter>("Log", out logText) {
                                                RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1), pivot = new Vector2(0, 1) },
                                                Component1  = { fontSize = 24, lineSpacing = 1, supportRichText = true, alignment = TextAnchor.UpperLeft, horizontalOverflow = HorizontalWrapMode.Wrap, verticalOverflow = VerticalWrapMode.Truncate  },
                                                Component2 = { horizontalFit = ContentSizeFitter.FitMode.Unconstrained, verticalFit = ContentSizeFitter.FitMode.PreferredSize },
                                                SetTarget = self => { logList.content = self.GetComponent<RectTransform>(); }
                                            },
                                            new Builder<Scrollbar,Image>("LogScrollbar", out refLogScrollbar) {
                                                RectTransform = { anchorMin = new Vector2(1, 0), anchorMax = new Vector2(1, 1) },
                                                Component1 = { navigation = new Navigation{ mode = Navigation.Mode.None }, direction = Scrollbar.Direction.BottomToTop, size = 1.0f },
                                                Component2 = { sprite = background, type = Image.Type.Sliced },
                                                SetTarget = self => { logList.verticalScrollbar = self.GetComponent<Scrollbar>(); },
                                                Child = new Builder<RectTransform>("Sliding Area2") {
                                                    RectTransform = { anchorMin = new Vector2(0, 0), anchorMax = new Vector2(1, 1) },
                                                    Child = new Builder<Image>("Handle2") {
                                                        Component1 = { sprite = uisprite, type = Image.Type.Sliced },
                                                        SetTarget = self =>
                                                        {
                                                            refLogScrollbar.targetGraphic = self.GetComponent<Graphic>();
                                                            refLogScrollbar.handleRect = self.GetComponent<RectTransform>();
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    },
                                }
                            }
                        }
                    }
                }
        };

        // size modify after build complete:)
        {
            var rect = GameObject.Find("HorizontalSplitter").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
        }
        {
            var rect = GameObject.Find("ListLayoutToAttach").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
        }
        {
            var rect = GameObject.Find("ListScrollbar").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(30, 0);
        }
        {
            var rect = GameObject.Find("ClearButton").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
        }
        {
            var rect = GameObject.Find("Sliding Area").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(-20, -20);
        }
        {
            var rect = GameObject.Find("Handle").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(20, 20);
        }
        {
            var rect = GameObject.Find("ButtonText").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
        }
        {
            var rect = GameObject.Find("Log").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(15, 0);
            rect.offsetMax = new Vector2(-20, 0);
        }
        {
            var rect = GameObject.Find("LogScrollbar").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(-30, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(30, 0);
        }
        {
            var rect = GameObject.Find("Sliding Area2").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(-20, -20);
        }
        {
            var rect = GameObject.Find("Handle2").GetComponent<RectTransform>();
            rect.offsetMin = new Vector2(0, 0);
            rect.offsetMax = new Vector2(0, 0);
            rect.sizeDelta = new Vector2(20, 20);
        }

        // add test script
        var runner = rootObject.GameObject.AddComponent<UnitTestRunner>();
        runner.clearButton = clearButton;
        runner.list = listLayout.gameObject.GetComponent<RectTransform>();
        runner.listScrollBar = refListScrollbar;
        runner.logText = logText;
        runner.logScrollBar = refLogScrollbar;

        return runner;
    }

    static void Build(string sceneName, string buildPath, RuntimeUnitTestSettings settings)
    {
        var options = BuildOptions.BuildScriptsOnly | BuildOptions.IncludeTestAssemblies;
        if (settings.AutoRunPlayer)
        {
            options |= BuildOptions.AutoRunPlayer;
        }
        if (settings.Headless)
        {
            options |= BuildOptions.EnableHeadlessMode;
        }

        var targetGroup = ToBuildTargetGroup(settings.BuildTarget);
        var currentBackend = PlayerSettings.GetScriptingBackend(targetGroup);
        if (currentBackend != settings.ScriptBackend)
        {
            UnityEngine.Debug.Log("Modify ScriptBackend to " + settings.ScriptBackend);
            PlayerSettings.SetScriptingBackend(targetGroup, settings.ScriptBackend);
        }

        var buildOptions = new BuildPlayerOptions
        {
            target = settings.BuildTarget,
            targetGroup = targetGroup,
            options = options,
            scenes = new[] { sceneName },
            locationPathName = buildPath
        };

        UnityEngine.Debug.Log("UnitTest Build Start, " + settings.ToString());

        var buildReport = BuildPipeline.BuildPlayer(buildOptions);

        if (currentBackend != settings.ScriptBackend)
        {
            UnityEngine.Debug.Log("Restore ScriptBackend to " + currentBackend);
            PlayerSettings.SetScriptingBackend(targetGroup, currentBackend);
        }

        if (buildReport.summary.result != BuildResult.Succeeded)
        {
            // Note: show error summary?
            // Debug.LogError(buildReport.SummarizeErrors());
            UnityEngine.Debug.LogError("UnitTest Build Failed.");
        }
        else
        {
            UnityEngine.Debug.Log("UnitTest Build Completed, binary located: " + buildOptions.locationPathName);
        }
    }

    static Color FromRGB(int r, int g, int b)
    {
        return new Color(r / 255f, g / 255f, b / 255f);
    }

    static bool IsWindows(BuildTarget buildTarget)
    {
        switch (buildTarget)
        {
            case BuildTarget.StandaloneWindows:
            case BuildTarget.StandaloneWindows64:
            case BuildTarget.WSAPlayer:
                return true;
            default:
                return false;
        }
    }

    static BuildTargetGroup ToBuildTargetGroup(BuildTarget buildTarget)
    {
#pragma warning disable CS0618
        switch (buildTarget)
        {
            case BuildTarget.StandaloneOSX:
            case (BuildTarget)3:
            case BuildTarget.StandaloneOSXIntel:
            case BuildTarget.StandaloneOSXIntel64:
            case BuildTarget.StandaloneWindows:
            case BuildTarget.StandaloneWindows64:
            case BuildTarget.StandaloneLinux:
            case BuildTarget.StandaloneLinux64:
            case BuildTarget.StandaloneLinuxUniversal:
                return BuildTargetGroup.Standalone;
            case (BuildTarget)6:
            case (BuildTarget)7:
            case BuildTarget.WebGL:
                return BuildTargetGroup.WebGL;
            case BuildTarget.iOS:
                return BuildTargetGroup.iOS;
            case BuildTarget.PS3:
                return BuildTargetGroup.PS3;
            case BuildTarget.PS4:
                return BuildTargetGroup.PS4;
            case BuildTarget.XBOX360:
                return BuildTargetGroup.XBOX360;
            case BuildTarget.Android:
                return BuildTargetGroup.Android;
            case BuildTarget.WSAPlayer:
                return BuildTargetGroup.WSA;
            case BuildTarget.WP8Player:
                return BuildTargetGroup.WP8;
            case BuildTarget.Tizen:
                return BuildTargetGroup.Tizen;
            case BuildTarget.PSP2:
                return BuildTargetGroup.PSP2;
            case BuildTarget.PSM:
                return BuildTargetGroup.PSM;
            case BuildTarget.XboxOne:
                return BuildTargetGroup.XboxOne;
            case BuildTarget.SamsungTV:
                return BuildTargetGroup.SamsungTV;
            case BuildTarget.N3DS:
                return BuildTargetGroup.N3DS;
            case BuildTarget.WiiU:
                return BuildTargetGroup.WiiU;
            case BuildTarget.tvOS:
                return BuildTargetGroup.tvOS;
            case BuildTarget.Switch:
                return BuildTargetGroup.Switch;
            case BuildTarget.Lumin:
                return BuildTargetGroup.Lumin;
            case BuildTarget.BlackBerry:
                return BuildTargetGroup.BlackBerry;
            case BuildTarget.NoTarget:
            default:
                return BuildTargetGroup.Unknown;
        }
#pragma warning restore CS0618

    }
}

#endif
