using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEngine;

namespace Davanci.Nexus.Editor
{
    public struct EventCallSite
    {
        public string AssetPath;
        public int Line;
    }

    // A call the 1.4.0 events replace, or an SDK called directly instead of through DavanciNexus.
    public struct EventWarning
    {
        public string AssetPath;
        public int Line;
        public string Message;
    }

    public sealed class EventUsage
    {
        public string Method;
        public readonly List<EventCallSite> Sites = new List<EventCallSite>();
        public bool IsCalled => Sites.Count > 0;
    }

    // Greps the project's C# scripts for DavanciNexus.* calls to report which events
    // are actually wired in. The package's own scripts are excluded.
    public static class NexusEventUsageScanner
    {
        static readonly string[] TrackedMethods =
        {
            "TutorialBegin", "TutorialStep", "TutorialStepDone", "TutorialComplete", "FtueStall",
            "Progress.Start", "Progress.Complete", "Progress.Fail",
            "CurrencyEarned", "CurrencySpent",
            "Unlocked", "Acquired", "Upgraded", "Milestone", "Reset",
            "OfflineEarningsCollected", "BoostActivated",
            "Checkpoint", "SetUserProperty",
            "AdWatched", "AdFailed",
            "Purchase", "Custom",
        };

        // Facade shortcuts that are remote config, not analytics. NexusRemoteConfigUsageScanner
        // reports these instead.
        static readonly string[] NonEventMethods =
        {
            "GetString", "GetBool", "GetInt", "GetFloat", "GetVariant",
        };

        static readonly Regex CallPattern = new Regex(@"\bDavanciNexus\.((?:Progress\.)?\w+)\s*\(", RegexOptions.Compiled);

        static readonly Regex TutorialStepPattern = new Regex(@"\bDavanciNexus\.TutorialStep\s*\(", RegexOptions.Compiled);
        static readonly Regex LevelStartPattern = new Regex(@"\bDavanciNexus\.LevelStart\s*\(", RegexOptions.Compiled);
        static readonly Regex DirectSdkPattern =
            new Regex(@"\b(GameAnalytics\.NewDesignEvent|FirebaseAnalytics\.LogEvent)\s*\(", RegexOptions.Compiled);

        // Third-party SDK folders call their own APIs; only the game's scripts are checked.
        static readonly string[] SdkFolders =
        {
            "/GameAnalytics/", "/Firebase/", "/AppsFlyer/", "/MaxSdk/", "/LevelPlay/",
            "/ExternalDependencyManager/", "/GooglePlayGames/", "/Plugins/",
        };

        public static List<EventUsage> Scan()
        {
            var byMethod = new Dictionary<string, EventUsage>();
            foreach (var method in TrackedMethods)
                byMethod[method] = new EventUsage { Method = method };

            foreach (var file in EnumerateGameScripts())
            {
                string[] lines = File.ReadAllLines(file);
                string assetPath = ToAssetPath(file);

                for (int i = 0; i < lines.Length; i++)
                {
                    foreach (Match match in CallPattern.Matches(lines[i]))
                    {
                        string method = match.Groups[1].Value;
                        if (System.Array.IndexOf(NonEventMethods, method) >= 0) continue;

                        if (!byMethod.TryGetValue(method, out var usage))
                            byMethod[method] = usage = new EventUsage { Method = method };

                        usage.Sites.Add(new EventCallSite { AssetPath = assetPath, Line = i + 1 });
                    }
                }
            }

            var result = new List<EventUsage>();
            foreach (var method in TrackedMethods)
                result.Add(byMethod[method]);

            foreach (var pair in byMethod)
                if (System.Array.IndexOf(TrackedMethods, pair.Key) < 0)
                    result.Add(pair.Value);

            return result;
        }

        public static List<EventWarning> ScanWarnings()
        {
            var warnings = new List<EventWarning>();
            foreach (var file in EnumerateGameScripts())
            {
                string path = file.Replace('\\', '/');
                bool sdkFile = false;
                foreach (var folder in SdkFolders)
                    if (path.Contains(folder)) { sdkFile = true; break; }
                if (sdkFile) continue;

                string[] lines = File.ReadAllLines(file);
                string assetPath = ToAssetPath(file);
                for (int i = 0; i < lines.Length; i++)
                {
                    string line = lines[i];
                    if (line.TrimStart().StartsWith("//")) continue;

                    foreach (Match m in TutorialStepPattern.Matches(line))
                        if (HasSingleArgument(line, m.Index + m.Length))
                            Add(warnings, assetPath, i, "TutorialStep(step) without a step name: use TutorialStep(step, \"buy_ride\").");
                    if (LevelStartPattern.IsMatch(line))
                        Add(warnings, assetPath, i, "LevelStart is deprecated: use DavanciNexus.Progress.Start(index, kind).");
                    foreach (Match m in DirectSdkPattern.Matches(line))
                        Add(warnings, assetPath, i, $"{m.Groups[1].Value} called directly: send it through DavanciNexus so every game reports it the same way.");
                }
            }
            return warnings;
        }

        static void Add(List<EventWarning> warnings, string assetPath, int index, string message)
            => warnings.Add(new EventWarning { AssetPath = assetPath, Line = index + 1, Message = message });

        // True when the call that opens at `start` closes on the same line with one argument.
        // A call spread over several lines is not judged.
        static bool HasSingleArgument(string line, int start)
        {
            int depth = 0;
            bool any = false;
            for (int i = start; i < line.Length; i++)
            {
                char c = line[i];
                if (c == '(' || c == '[' || c == '{') depth++;
                else if (c == ')' || c == ']' || c == '}')
                {
                    if (depth == 0) return any;
                    depth--;
                }
                else if (c == ',' && depth == 0) return false;
                else if (!char.IsWhiteSpace(c)) any = true;
            }
            return false;
        }

        internal static IEnumerable<string> EnumerateGameScripts()
        {
            string root = Application.dataPath;
            foreach (var file in Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories))
            {
                string path = file.Replace('\\', '/');
                // The package and its samples are not the game: their demo keys and event calls
                // would otherwise read as the developer's own.
                if (path.Contains("/DavanciNexus/") || path.Contains("/DavanciNexusAuthor/"))
                    continue;
                yield return file;
            }
        }

        internal static string ToAssetPath(string absolutePath)
        {
            absolutePath = absolutePath.Replace('\\', '/');
            int index = absolutePath.IndexOf("/Assets/", System.StringComparison.Ordinal);
            return index >= 0 ? absolutePath.Substring(index + 1) : absolutePath;
        }
    }
}
