using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using VRTK;

[InitializeOnLoad]
public sealed class VRTK_UpdatePrompt : EditorWindow
{
    [Serializable]
    private sealed class LatestRelease
    {
        [NonSerialized]
        public Version version;
        [NonSerialized]
        public DateTime publishedDateTime;
        [NonSerialized]
        public List<string> changelogPages;

#pragma warning disable 649
        public string html_url;
        public string tag_name;
        public string name;
        public string published_at;
        public string zipball_url;
        public string body;
#pragma warning restore 649

        public static LatestRelease CreateFromJSON(string json)
        {
            LatestRelease latestRelease = JsonUtility.FromJson<LatestRelease>(json);
            latestRelease.version = new Version(latestRelease.tag_name);
            latestRelease.publishedDateTime = DateTime.Parse(latestRelease.published_at);

            string changelog = latestRelease.body;
            latestRelease.body = null;

            changelog = Regex.Replace(changelog, @"(?<!(\r\n){2}) \*.*\*{2}(.*)\*{2}", "\n<size=13>$2</size>");
            changelog = Regex.Replace(changelog, @"(\r\n){2} \*.*\*{2}(.*)\*{2}", "\n\n<size=13>$2</size>");
            changelog = new Regex(@"(#+)\s?(.*)\b").Replace(
                changelog,
                match => string.Format(
                    "<size={0}>{1}</size>",
                    30 - match.Groups[1].Value.Length * 6,
                    match.Groups[2].Value
                )
            );
            changelog = changelog.Replace("  *", "*");

            // Each char gets turned into two triangles and the mesh vertices limit is 2^16.
            // Let's use another 100 to be on the safe side.
            const int textLengthLimit = 65536 / 4 - 100;
            latestRelease.changelogPages = new List<string>((int)Mathf.Ceil(changelog.Length / (float)textLengthLimit));

            while (true)
            {
                if (changelog.Length > textLengthLimit)
                {
                    int startIndex = Math.Min(changelog.Length, textLengthLimit);
                    int lastIndexOf = changelog.LastIndexOf("\n", startIndex, StringComparison.Ordinal);

                    if (lastIndexOf == -1)
                    {
                        lastIndexOf = startIndex;
                    }

                    latestRelease.changelogPages.Add(changelog.Substring(0, lastIndexOf));
                    changelog = changelog.Substring(lastIndexOf).TrimStart('\n', '\r');
                }
                else
                {
                    latestRelease.changelogPages.Add(changelog);
                    break;
                }
            }

            return latestRelease;
        }
    }

    private const string remoteURL = "https://api.github.com/repos/thestonefox/vrtk/releases/latest";
    private const string assetStoreURL = "/content/64131";
    private const string hidePromptKeyFormat = "VRTK.HideVersionPrompt.v{0}";
    private const string lastCheckKey = "VRTK.VersionPromptUpdate";
    private const int checkUpdateHours = 6;

    private static bool isManualCheck;
    private static bool versionChecked;
    private static WWW versionResource;
    private static LatestRelease latestRelease;
    private static VRTK_UpdatePrompt promptWindow;

    private static Vector2 scrollPosition;
    private static Vector2 changelogScrollPosition;
    private static float changelogWidth;
    private static int changelogPageIndex;
    private static bool isChangelogFoldOut = true;

    static VRTK_UpdatePrompt()
    {
        EditorApplication.update += CheckForUpdate;
    }

    public void OnGUI()
    {
        using (GUILayout.ScrollViewScope scrollViewScope = new GUILayout.ScrollViewScope(scrollPosition))
        {
            scrollPosition = scrollViewScope.scrollPosition;

            if (versionResource != null && !versionResource.isDone)
            {
                EditorGUILayout.HelpBox("Checking for updates...", MessageType.Info);
                return;
            }

            if (latestRelease == null)
            {
                EditorGUILayout.HelpBox("There was a problem checking for updates.", MessageType.Error);
                DrawCheckAgainButton();

                return;
            }

            string newVersionName = latestRelease.name == string.Format("Version {0}", latestRelease.version)
                                        ? string.Empty
                                        : string.Format(" \"{0}\"", latestRelease.name);
            bool isUpToDate = VRTK_Defines.CurrentVersion >= latestRelease.version;

            EditorGUILayout.HelpBox(
                string.Format(
                    "{0}.\n\nInstalled Version: {1}\nAvailable version: {2}{3} (published on {4})",
                    isUpToDate ? "Already up to date" : "A new version of VRTK is available",
                    VRTK_Defines.CurrentVersion,
                    latestRelease.version,
                    newVersionName,
                    latestRelease.publishedDateTime.ToLocalTime()),
                isUpToDate ? MessageType.Info : MessageType.Warning);

            DrawCheckAgainButton();

            isChangelogFoldOut = EditorGUILayout.Foldout(isChangelogFoldOut, "Changelog", true);
            if (isChangelogFoldOut)
            {
                using (new EditorGUILayout.HorizontalScope())
                {
                    GUILayout.Space(10);

                    using (new EditorGUILayout.VerticalScope())
                    {
                        VRTK_EditorUtilities.DrawScrollableSelectableLabel(
                            ref changelogScrollPosition,
                            ref changelogWidth,
                            latestRelease.changelogPages[changelogPageIndex],
                            new GUIStyle(EditorStyles.textArea)
                            {
                                richText = true
                            });

                        if (latestRelease.changelogPages.Count > 0)
                        {
                            using (new EditorGUILayout.HorizontalScope())
                            {
                                using (new EditorGUI.DisabledGroupScope(changelogPageIndex == 0))
                                {
                                    if (GUILayout.Button("Previous Page"))
                                    {
                                        changelogPageIndex = Math.Max(0, --changelogPageIndex);
                                        changelogScrollPosition = Vector3.zero;
                                    }
                                }
                                using (new EditorGUI.DisabledGroupScope(changelogPageIndex == latestRelease.changelogPages.Count - 1))
                                {
                                    if (GUILayout.Button("Next Page"))
                                    {
                                        changelogPageIndex = Math.Min(latestRelease.changelogPages.Count - 1, ++changelogPageIndex);
                                        changelogScrollPosition = Vector3.zero;
                                    }
                                }
                            }
                        }

                        if (GUILayout.Button("View on GitHub"))
                        {
                            Application.OpenURL(latestRelease.html_url);
                        }
                    }
                }
            }

            if (isUpToDate)
            {
                return;
            }

            GUILayout.FlexibleSpace();

            VRTK_EditorUtilities.AddHeader("Get Latest Version");
            using (new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button("From Asset Store"))
                {
                    AssetStore.Open(assetStoreURL);
                    Close();
                }
                if (GUILayout.Button("From GitHub"))
                {
                    Application.OpenURL(latestRelease.zipball_url);
                }
            }

            using (EditorGUI.ChangeCheckScope changeCheckScope = new EditorGUI.ChangeCheckScope())
            {
                string key = string.Format(hidePromptKeyFormat, latestRelease.version);
                bool hideToggle = EditorPrefs.HasKey(key);

                hideToggle = GUILayout.Toggle(hideToggle, "Do not prompt for this version again.");

                if (changeCheckScope.changed)
                {
                    if (hideToggle)
                    {
                        EditorPrefs.SetBool(key, true);
                    }
                    else
                    {
                        EditorPrefs.DeleteKey(key);
                    }
                }
            }
        }
    }

    private static void DrawCheckAgainButton()
    {
        if (GUILayout.Button("Check again"))
        {
            CheckManually();
        }
    }

    private static void CheckForUpdate()
    {
        changelogScrollPosition = Vector3.zero;
        changelogWidth = 0;
        changelogPageIndex = 0;

        if (isManualCheck)
        {
            ShowWindow();
        }
        else
        {
            if (versionChecked)
            {
                EditorApplication.update -= CheckForUpdate;
                return;
            }

            if (EditorPrefs.HasKey(lastCheckKey))
            {
                string lastCheckTicksString = EditorPrefs.GetString(lastCheckKey);
                DateTime lastCheckDateTime = new DateTime(Convert.ToInt64(lastCheckTicksString));

                if (lastCheckDateTime.AddHours(checkUpdateHours) >= DateTime.UtcNow)
                {
                    versionChecked = true;
                    return;
                }
            }
        }

        versionResource = versionResource == null ? new WWW(remoteURL) : versionResource;
        if (!versionResource.isDone)
        {
            return;
        }

        EditorApplication.update -= CheckForUpdate;

        if (string.IsNullOrEmpty(versionResource.error))
        {
            latestRelease = LatestRelease.CreateFromJSON(versionResource.text);
        }

        versionResource.Dispose();
        versionResource = null;
        versionChecked = true;
        EditorPrefs.SetString(lastCheckKey, DateTime.UtcNow.Ticks.ToString());

        // Clean up the existing hidePromptKeys (except the one for the current version)
        new[] { VRTK_Defines.CurrentVersion }
            .Concat(VRTK_Defines.PreviousVersions)
            .Where(version => latestRelease == null || version != latestRelease.version)
            .Select(version => string.Format(hidePromptKeyFormat, version))
            .Where(EditorPrefs.HasKey)
            .ToList()
            .ForEach(EditorPrefs.DeleteKey);

        if (!isManualCheck
            && latestRelease != null
            && (VRTK_Defines.CurrentVersion >= latestRelease.version
                || EditorPrefs.HasKey(string.Format(hidePromptKeyFormat, latestRelease.version))))
        {
            return;
        }

        ShowWindow();
        isManualCheck = false;
    }

    private static void ShowWindow()
    {
        if (promptWindow != null)
        {
            promptWindow.ShowUtility();
            promptWindow.Repaint();
            return;
        }

        promptWindow = GetWindow<VRTK_UpdatePrompt>(true);
        promptWindow.titleContent = new GUIContent("VRTK Update");
    }

    [MenuItem("Window/VRTK/Check for Updates")]
    private static void CheckManually()
    {
        isManualCheck = true;

        if (versionResource == null)
        {
            EditorApplication.update += CheckForUpdate;
        }
    }
}
