using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Xml;

namespace TeslaConsole.DiffTests
{
    /// <summary>
    /// Lives inside a child AppDomain whose base directory is the directory of one
    /// TeslaConsole.exe. It loads that exe and invokes deterministic, dependency-free
    /// methods by reflection, returning a normalized string so results marshal cleanly
    /// across the AppDomain boundary.
    ///
    /// Only methods whose code path touches nothing but the framework + the exe itself
    /// are exercised, so the original (which ships without its proprietary dependency
    /// DLLs) loads and runs them fine.
    /// </summary>
    public sealed class Invoker : MarshalByRefObject
    {
        private readonly Assembly _asm;
        private readonly string _probeDir;

        public Invoker(string exePath, string probeDir)
        {
            _probeDir = probeDir;
            // The original ships without its proprietary dependency DLLs; resolve any
            // referenced assembly out of the recovered build's output directory so both
            // assemblies expose the same metadata.
            AppDomain.CurrentDomain.AssemblyResolve += ResolveFromProbeDir;
            _asm = Assembly.LoadFrom(exePath);
        }

        private Assembly ResolveFromProbeDir(object sender, ResolveEventArgs args)
        {
            var name = new AssemblyName(args.Name);
            if (string.Equals(name.Name, "TeslaConsole", StringComparison.OrdinalIgnoreCase))
                return _asm; // self-references bind to the assembly under test
            if (string.IsNullOrEmpty(_probeDir)) return null;
            foreach (var ext in new[] { ".dll", ".exe" })
            {
                string candidate = Path.Combine(_probeDir, name.Name + ext);
                if (File.Exists(candidate))
                    return Assembly.LoadFrom(candidate);
            }
            return null;
        }

        /// <summary>Full identity of the loaded assembly (for sanity assertions).</summary>
        public string AssemblyFullName => _asm.FullName;

        // ---- Structural (public API surface) enumeration, run inside this domain ----

        public string[] GetPublicTypeNames()
        {
            return SafeGetTypes()
                .Where(t => (t.IsPublic || t.IsNestedPublic) && !IsCompilerGenerated(t))
                .Select(t => t.FullName)
                .Distinct()
                .ToArray();
        }

        public string[] GetPublicMemberSignatures()
        {
            var set = new HashSet<string>();
            const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance |
                                       BindingFlags.Static | BindingFlags.DeclaredOnly;
            foreach (var t in SafeGetTypes())
            {
                if (!(t.IsPublic || t.IsNestedPublic) || IsCompilerGenerated(t))
                    continue;
                foreach (var m in t.GetMembers(flags))
                {
                    if (m.Name.IndexOf('<') >= 0 || IsCompilerGenerated(m))
                        continue;
                    // Skip property/event accessor methods: the property/event itself is
                    // compared via its own entry, and Roslyn vs. the original compiler mark
                    // these accessors' [CompilerGenerated] differently. Real operators
                    // (op_*) are kept.
                    if (IsAccessorMethod(m))
                        continue;
                    try { set.Add(t.FullName + " :: " + DescribeMember(m)); }
                    catch { /* unresolved member metadata; skip symmetrically */ }
                }
            }
            return set.ToArray();
        }

        private IEnumerable<Type> SafeGetTypes()
        {
            try { return _asm.GetTypes(); }
            catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t => t != null); }
        }

        private static string DescribeMember(MemberInfo m)
        {
            switch (m)
            {
                case MethodInfo mi:
                    return "M " + mi.ReturnType.FullName + " " + mi.Name +
                           "(" + string.Join(",", mi.GetParameters().Select(p => p.ParameterType.FullName)) + ")";
                case ConstructorInfo ci:
                    return "C .ctor(" +
                           string.Join(",", ci.GetParameters().Select(p => p.ParameterType.FullName)) + ")";
                case PropertyInfo pi:
                    return "P " + pi.PropertyType.FullName + " " + pi.Name;
                case FieldInfo fi:
                    return "F " + fi.FieldType.FullName + " " + fi.Name;
                case EventInfo ei:
                    return "E " + ei.EventHandlerType.FullName + " " + ei.Name;
                case Type nt:
                    return "T " + nt.FullName;
                default:
                    return m.MemberType + " " + m.Name;
            }
        }

        private static bool IsAccessorMethod(MemberInfo m)
        {
            if (!(m is MethodInfo mi) || !mi.IsSpecialName) return false;
            string n = mi.Name;
            return n.StartsWith("get_") || n.StartsWith("set_") ||
                   n.StartsWith("add_") || n.StartsWith("remove_");
        }

        private static bool IsCompilerGenerated(MemberInfo m)
        {
            try { return m.IsDefined(typeof(CompilerGeneratedAttribute), false); }
            catch { return false; }
        }

        /// <summary>
        /// Dispatch a named case. Returns the method's normalized result, or
        /// "EXC:&lt;ExceptionTypeName&gt;" when the underlying call throws — so exception
        /// behavior is compared too.
        /// </summary>
        public string Run(string caseName, string[] args)
        {
            try
            {
                return Dispatch(caseName, args);
            }
            catch (TargetInvocationException tie)
            {
                return "EXC:" + (tie.InnerException ?? tie).GetType().Name;
            }
            catch (Exception ex)
            {
                return "EXC:" + ex.GetType().Name;
            }
        }

        private string Dispatch(string caseName, string[] args)
        {
            switch (caseName)
            {
                case "GetTimeString":
                    return RPStringsGetTimeString(long.Parse(args[0], CultureInfo.InvariantCulture));

                case "HostTypeParse":
                    return HostTypeParse(args[0]);

                case "ConvertBitmap":
                    return ConvertBitmap(
                        int.Parse(args[0], CultureInfo.InvariantCulture),
                        int.Parse(args[1], CultureInfo.InvariantCulture),
                        int.Parse(args[2], CultureInfo.InvariantCulture));

                case "GenerateString":
                    return GenerateString(
                        int.Parse(args[0], CultureInfo.InvariantCulture),
                        int.Parse(args[1], CultureInfo.InvariantCulture),
                        args[2], args[3]);

                case "RPMapParse":
                    return ParseRedPlanetNode("TeslaConsole.RedPlanet.RPMap", args[0]);

                case "RPVehicleParse":
                    return ParseRedPlanetNode("TeslaConsole.RedPlanet.RPVehicle", args[0]);

                case "SiteManagementGuid":
                    return SiteManagementGuid(args[0]);

                case "TupleCreate":
                    return TupleCreate(args[0], args[1]);

                case "CatalogSummary":
                    return CatalogSummary(args[0]);

                case "CatalogEntry":
                    return CatalogEntry(args[0], args[1], args[2], args[3]);

                default:
                    throw new ArgumentException("Unknown case: " + caseName);
            }
        }

        private Type T(string fullName)
        {
            return _asm.GetType(fullName, throwOnError: true);
        }

        private string RPStringsGetTimeString(long ticks)
        {
            var m = T("TeslaConsole.RPStrings").GetMethod(
                "GetTimeString", BindingFlags.Public | BindingFlags.Static);
            return (string)m.Invoke(null, new object[] { TimeSpan.FromTicks(ticks) });
        }

        private string HostTypeParse(string input)
        {
            var helperType = T("TeslaConsole.HostTypeHelper");
            var parse = helperType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static);
            object helper = parse.Invoke(null, new object[] { input });
            return helper.ToString();
        }

        private string ConvertBitmap(int width, int height, int seed)
        {
            using (var bmp = MakePatternBitmap(width, height, seed))
            {
                var m = T("TeslaConsole.PlasmaBitmaps").GetMethod(
                    "ConvertBitmap", BindingFlags.Public | BindingFlags.Static);
                return (string)m.Invoke(null, new object[] { bmp });
            }
        }

        private string GenerateString(int width, int height, string font, string text)
        {
            var m = T("TeslaConsole.PlasmaBitmaps").GetMethod(
                "GenerateString",
                BindingFlags.Public | BindingFlags.Static,
                null,
                new[] { typeof(int), typeof(int), typeof(string), typeof(string) },
                null);
            return (string)m.Invoke(null, new object[] { width, height, font, text });
        }

        private string ParseRedPlanetNode(string typeName, string xml)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xml);
            XmlNode node = doc.DocumentElement;

            var type = T(typeName);
            var ctor = type.GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance,
                null, new[] { typeof(XmlNode) }, null);
            object instance = ctor.Invoke(new object[] { node });

            string key = (string)type.GetProperty("Key").GetValue(instance, null);
            string name = (string)type.GetProperty("Name").GetValue(instance, null);
            string toStr = instance.ToString();
            return key + "|" + name + "|" + toStr;
        }

        private string SiteManagementGuid(string fieldName)
        {
            var f = T("TeslaConsole.SiteManagement").GetField(
                fieldName, BindingFlags.Public | BindingFlags.Static);
            return ((Guid)f.GetValue(null)).ToString("D");
        }

        private string TupleCreate(string a, string b)
        {
            // Tuple.Create<int, string>(int.Parse(a), b) -> "A|B"
            var tupleType = T("TeslaConsole.Tuple");
            MethodInfo create = null;
            foreach (var m in tupleType.GetMethods(BindingFlags.Public | BindingFlags.Static))
            {
                if (m.Name == "Create" && m.IsGenericMethodDefinition &&
                    m.GetGenericArguments().Length == 2 && m.GetParameters().Length == 2)
                {
                    create = m;
                    break;
                }
            }
            var closed = create.MakeGenericMethod(typeof(int), typeof(string));
            object tuple = closed.Invoke(null, new object[] { int.Parse(a, CultureInfo.InvariantCulture), b });

            Type tt = tuple.GetType();
            object av = tt.GetProperty("A").GetValue(tuple, null);
            object bv = tt.GetProperty("B").GetValue(tuple, null);
            return Convert.ToString(av, CultureInfo.InvariantCulture) + "|" +
                   Convert.ToString(bv, CultureInfo.InvariantCulture);
        }

        private System.Collections.IEnumerable LoadCatalog(string path)
        {
            var reg = T("TeslaConsole.AppRegistry");
            reg.GetMethod("LoadFromFile", BindingFlags.Public | BindingFlags.Static)
               .Invoke(null, new object[] { path });
            return (System.Collections.IEnumerable)reg.GetProperty("Products").GetValue(null);
        }

        private string CatalogSummary(string path)
        {
            int products = 0, entries = 0;
            foreach (var product in LoadCatalog(path))
            {
                products++;
                var list = (System.Collections.IEnumerable)product.GetType().GetField("Entries").GetValue(product);
                foreach (var _ in list) entries++;
            }
            return $"products={products};entries={entries}";
        }

        /// <summary>
        /// Finds the launch entry with the given key, builds its LaunchData with the optional
        /// resolution, and returns "DisplayName|LaunchKey|Exe|Args|WorkingDirectory|AutoRestart".
        /// </summary>
        private string CatalogEntry(string path, string launchKey, string resW, string resH)
        {
            Guid key = Guid.Parse(launchKey);
            object entry = null;
            foreach (var product in LoadCatalog(path))
            {
                var entries = (System.Collections.IEnumerable)product.GetType().GetField("Entries").GetValue(product);
                foreach (var en in entries)
                {
                    if ((Guid)en.GetType().GetField("LaunchKey").GetValue(en) == key) { entry = en; break; }
                }
                if (entry != null) break;
            }
            if (entry == null) return "NOTFOUND";

            object resolution = null;
            if (resW.Length > 0 && resH.Length > 0)
                resolution = new Size(int.Parse(resW, CultureInfo.InvariantCulture),
                                      int.Parse(resH, CultureInfo.InvariantCulture));

            object ld = entry.GetType().GetMethod("ToLaunchData").Invoke(entry, new object[] { resolution });
            Type t = ld.GetType();
            object pair = t.GetField("LaunchPair").GetValue(ld);
            string disp = (string)pair.GetType().GetField("DisplayName").GetValue(pair);
            Guid lkey = (Guid)pair.GetType().GetField("LaunchKey").GetValue(pair);
            string exe = (string)t.GetField("ExeFile").GetValue(ld);
            string margs = (string)t.GetField("Arguments").GetValue(ld);
            string wd = (string)t.GetField("WorkingDirectory").GetValue(ld);
            bool ar = (bool)t.GetField("AutoRestart").GetValue(ld);
            return $"{disp}|{lkey:D}|{exe}|{margs}|{wd}|{ar}";
        }

        /// <summary>
        /// Deterministic black/white bitmap whose pixels depend only on (x, y, seed),
        /// so both assemblies receive identical input pixels to pack.
        /// </summary>
        private static Bitmap MakePatternBitmap(int width, int height, int seed)
        {
            var bmp = new Bitmap(width, height);
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    bool on = ((x * 31 + y * 17 + seed * 7) & 3) == 0;
                    bmp.SetPixel(x, y, on ? Color.White : Color.Black);
                }
            }
            return bmp;
        }

        // Keep the proxy alive for the life of the AppDomain.
        public override object InitializeLifetimeService() => null;
    }
}
