using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
#if HARMONY_2
using HarmonyLib;
#else
using Harmony;
#endif

namespace StardewModdingAPI.Framework.Commands
{
    /// <summary>The 'harmony_summary' SMAPI console command.</summary>
    internal class HarmonySummaryCommand : IInternalCommand
    {
#if !HARMONY_2
        /*********
        ** Fields
        *********/
        /// <summary>The Harmony instance through which to fetch patch info.</summary>
        private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}");
#endif

        /*********
        ** Accessors
        *********/
        /// <summary>The command name, which the user must type to trigger it.</summary>
        public string Name { get; } = "harmony_summary";

        /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary>
        public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary <search>\n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method.";


        /*********
        ** Public methods
        *********/
        /// <summary>Handle the console command when it's entered by the user.</summary>
        /// <param name="args">The command arguments.</param>
        /// <param name="monitor">Writes messages to the console.</param>
        public void HandleCommand(string[] args, IMonitor monitor)
        {
            SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).ToArray();

            StringBuilder result = new StringBuilder();

            if (!matches.Any())
                result.AppendLine("No current patches match your search.");
            else
            {
                result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:");
                result.AppendLine();
                foreach (var match in matches)
                {
                    result.AppendLine($"   {match.MethodName}");
                    foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key))
                    {
                        var sortedTypes = ownerGroup.Value
                            .OrderBy(p => p switch
                            {
                                PatchType.Prefix => 0,
                                PatchType.Postfix => 1,
#if HARMONY_2
                                PatchType.Finalizer => 2,
#endif
                                PatchType.Transpiler => 3,
                                _ => 4
                            });

                        result.AppendLine($"      - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})");
                    }
                    result.AppendLine();
                }
            }

            monitor.Log(result.ToString().TrimEnd(), LogLevel.Info);
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Get all current Harmony patches matching any of the given search terms.</summary>
        /// <param name="searchTerms">The search terms to match.</param>
        private IEnumerable<SearchResult> FilterPatches(string[] searchTerms)
        {
            bool hasSearch = searchTerms.Any();
            bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1);

            foreach (var patch in this.GetAllPatches())
            {
                // matches entire patch
                if (IsMatch(patch.MethodDescription))
                {
                    yield return patch;
                    continue;
                }

                // matches individual patchers
                foreach (var pair in patch.PatchTypesByOwner.ToArray())
                {
                    if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString())))
                        patch.PatchTypesByOwner.Remove(pair.Key);
                }

                if (patch.PatchTypesByOwner.Any())
                    yield return patch;
            }
        }

        /// <summary>Get all current Harmony patches.</summary>
        private IEnumerable<SearchResult> GetAllPatches()
        {
#if HARMONY_2
            foreach (MethodBase method in Harmony.GetAllPatchedMethods())
#else
            foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods())
#endif
            {
                // get metadata for method
#if HARMONY_2
                HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method);
#else
                Harmony.Patches patchInfo = this.HarmonyInstance.GetPatchInfo(method);
#endif

                IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>>
                {
                    [PatchType.Prefix] = patchInfo.Prefixes,
                    [PatchType.Postfix] = patchInfo.Postfixes,
#if HARMONY_2
                    [PatchType.Finalizer] = patchInfo.Finalizers,
#endif
                    [PatchType.Transpiler] = patchInfo.Transpilers
                };

                // get patch types by owner
                var typesByOwner = new Dictionary<string, ISet<PatchType>>();
                foreach (var group in patchGroups)
                {
                    foreach (var patch in group.Value)
                    {
                        if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes))
                            typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>();
                        patchTypes.Add(group.Key);
                    }
                }

                // create search result
                yield return new SearchResult(method, typesByOwner);
            }
        }

        /// <summary>A Harmony patch type.</summary>
        private enum PatchType
        {
            /// <summary>A prefix patch.</summary>
            Prefix,

            /// <summary>A postfix patch.</summary>
            Postfix,

#if HARMONY_2
            /// <summary>A finalizer patch.</summary>
            Finalizer,
#endif

            /// <summary>A transpiler patch.</summary>
            Transpiler
        }

        /// <summary>A patch search result for a method.</summary>
        private class SearchResult
        {
            /*********
            ** Accessors
            *********/
            /// <summary>A simple human-readable name for the patched method.</summary>
            public string MethodName { get; }

            /// <summary>A detailed description for the patched method.</summary>
            public string MethodDescription { get; }

            /// <summary>The patch types by the Harmony instance ID that added them.</summary>
            public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; }


            /*********
            ** Public methods
            *********/
            /// <summary>Construct an instance.</summary>
            /// <param name="method">The patched method.</param>
            /// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param>
            public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner)
            {
                this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}";
                this.MethodDescription = method.FullDescription();
                this.PatchTypesByOwner = patchTypesByOwner;
            }
        }
    }
}