using System;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Multiplayer.API;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using MultiplayerLoader;
using Verse;

namespace Multiplayer.Client
{
    public class SyncTransformer(Type networkType, Delegate writer, Delegate reader)
    {
        public Type networkType = networkType;
        public Delegate writer = writer;
        public Delegate reader = reader;
    }

    public delegate void SyncMethodWriter(object obj, SyncType type, string debugInfo);


    public class SyncMethod : SyncHandler, ISyncMethod, ISyncMethodForPrepatcher
    {
        public readonly Type targetType;
        public readonly MethodInfo method;
        public readonly FastInvokeHandler methodDelegate;

        protected readonly string instancePath;

        protected SyncTransformer targetTransformer;
        public SyncType[] argTypes;
        protected string[] argNames;
        protected SyncTransformer[] argTransformers;

        private int minTime = 100; // Milliseconds between resends
        private long lastSendTime;

        private bool cancelIfAnyArgNull;
        private bool cancelIfNoSelectedMapObjects;
        private bool cancelIfNoSelectedWorldObjects;

        private Action<object, object[]> beforeCall;
        private Action<object, object[]> afterCall;

        public Type TargetType => targetType;

        public SyncMethod(Type targetType, string instancePath, MethodInfo method, SyncType[] inTypes)
        {
            this.method = method;
            this.targetType = targetType;
            this.instancePath = instancePath;

            methodDelegate = MethodInvoker.GetHandler(method);
            argTypes = CheckArgs(method, inTypes);
            argNames = method.GetParameters().Names();
            argTransformers = new SyncTransformer[argTypes.Length];
        }

        private static SyncType[] CheckArgs(MethodInfo method, SyncType[] argTypes)
        {
            if (argTypes == null || argTypes.Length == 0)
                return method.GetParameters().Select(p => (SyncType)p).ToArray();
            else if (argTypes.Select(a => a.type).Zip(method.GetParameters().Types()).Any(t => t.a != t.b))
                throw new Exception($"Wrong parameter types for method: {method}");

            return argTypes;
        }

        /// <summary>
        /// Returns whether the original should be cancelled
        /// </summary>
        public bool DoSync(object target, params object[] args)
        {
            if (!Multiplayer.ShouldSync)
                return false;

            // todo limit per specific target/argument
            //if (Utils.MillisNow - lastSendTime < minTime)
            //    return true;

            LoggingByteWriter writer = new LoggingByteWriter();
            MpContext context = writer.MpContext();
            writer.Log.Node(ToString());

            writer.WriteInt32(syncId);
            SyncUtil.WriteContext(this, writer);

            var map = context.map;

            void SyncObj(object obj, SyncType type, string debugInfo)
            {
                writer.Log.Enter(debugInfo);

                try
                {
                    SyncSerialization.WriteSyncObject(writer, obj, type);
                }
                finally
                {
                    writer.Log.Exit();
                }

                if (type.contextMap && obj is Map contextMap)
                    map = contextMap;

                if (context.map is Map newMap)
                {
                    if (map != null && map != newMap)
                        throw new Exception($"{this}: map mismatch ({map?.uniqueID} and {newMap?.uniqueID})");
                    map = newMap;
                }
            }

            if (targetTransformer != null)
                SyncObj(targetTransformer.writer.DynamicInvoke(target, target, args), targetTransformer.networkType, "Target (transformed)");
            else
                WriteTarget(target, args, SyncObj);

            for (int i = 0; i < argTypes.Length; i++)
                if (argTransformers[i] == null)
                    SyncObj(args[i], argTypes[i], $"Arg {i} {argNames[i]}");

            for (int i = 0; i < argTypes.Length; i++)
                if (argTransformers[i] is { } trans)
                    SyncObj(trans.writer.DynamicInvoke(args[i], target, args), trans.networkType, $"Arg {i} {argNames[i]} (transformed)");

            int mapId = map?.uniqueID ?? ScheduledCommand.Global;
            writer.Log.Node("Map id: " + mapId);
            Multiplayer.WriterLog.AddCurrentNode(writer);

            SendSyncCommand(mapId, writer);

            lastSendTime = Utils.MillisNow;

            return true;
        }

        protected virtual void WriteTarget(object target, object[] args, SyncMethodWriter writer)
        {
            if (targetType != null)
                writer(target, targetType, "Target");
        }

        public override void Handle(ByteReader data)
        {
            object target;

            if (targetTransformer != null)
                target = targetTransformer.reader.DynamicInvoke(SyncSerialization.ReadSyncObject(data, targetTransformer.networkType));
            else
                target = ReadTarget(data);

            if (targetType != null && target == null)
            {
                MpLog.Debug($"SyncMethod {this} read target null");
                return;
            }

            if (!instancePath.NullOrEmpty())
                target = target.GetPropertyOrField(instancePath);

            var args = new object[argTypes.Length];

            for (int i = 0; i < argTypes.Length; i++)
                if (argTransformers[i] == null)
                    args[i] = SyncSerialization.ReadSyncObject(data, argTypes[i]);

            for (int i = 0; i < argTypes.Length; i++)
                if (argTransformers[i] is { } trans)
                    args[i] = trans.reader.DynamicInvoke(SyncSerialization.ReadSyncObject(data, trans.networkType));

            if (cancelIfAnyArgNull && args.Any(a => a == null))
                return;

            if (context.HasFlag(SyncContext.MapSelected) && cancelIfNoSelectedMapObjects && Find.Selector.selected.Count == 0)
                return;

            if (context.HasFlag(SyncContext.WorldSelected) && cancelIfNoSelectedWorldObjects && Find.WorldSelector.selected.Count == 0)
                return;

            beforeCall?.Invoke(target, args);

            MpLog.Debug($"Invoked {method} on {target} with {args.Length} params {args.ToStringSafeEnumerable()}");
            methodDelegate.Invoke(target, args);

            afterCall?.Invoke(target, args);
        }

        protected virtual object ReadTarget(ByteReader data)
        {
            if (targetType != null)
                return SyncSerialization.ReadSyncObject(data, targetType);

            return null;
        }

        public ISyncMethod MinTime(int time)
        {
            minTime = time;
            return this;
        }

        public ISyncMethod SetContext(SyncContext context)
        {
            this.context = context;
            return this;
        }

        public ISyncMethod SetVersion(int version)
        {
            this.version = version;
            return this;
        }

        public ISyncMethod SetDebugOnly()
        {
            debugOnly = true;
            return this;
        }

        public ISyncMethod SetHostOnly()
        {
            hostOnly = true;
            return this;
        }

        public ISyncMethod SetPreInvoke(Action<object, object[]> action)
        {
            beforeCall = action;
            return this;
        }

        public ISyncMethod SetPostInvoke(Action<object, object[]> action)
        {
            afterCall = action;
            return this;
        }

        public ISyncMethod CancelIfAnyArgNull()
        {
            cancelIfAnyArgNull = true;
            return this;
        }

        public ISyncMethod CancelIfNoSelectedMapObjects()
        {
            cancelIfNoSelectedMapObjects = true;
            return this;
        }

        public ISyncMethod CancelIfNoSelectedWorldObjects()
        {
            cancelIfNoSelectedWorldObjects = true;
            return this;
        }

        public ISyncMethod ExposeParameter(int index)
        {
            argTypes[index].expose = true;
            return this;
        }

        public ISyncMethod TransformArgument<Live, Networked>(int index, Serializer<Live, Networked> serializer, bool skipTypeCheck = false)
        {
            if (!skipTypeCheck && argTypes[index].type != typeof(Live))
                throw new Exception($"Arg transformer type mismatch for {this}: {argTypes[index].type} != {typeof(Live)}");

            argTransformers[index] = new(typeof(Networked), serializer.Writer, serializer.Reader);
            return this;
        }

        public ISyncMethod TransformTarget<Live, Networked>(Serializer<Live, Networked> serializer, bool skipTypeCheck = false)
        {
            if (!skipTypeCheck && targetType != typeof(Live))
                throw new Exception($"Target transformer type mismatch for {this}: {targetType} != {typeof(Live)}");

            targetTransformer = new(typeof(Networked), serializer.Writer, serializer.Reader);
            return this;
        }

        public static SyncMethod Register(Type type, string methodOrPropertyName, SyncType[] argTypes = null)
        {
            return Sync.RegisterSyncMethod(type, methodOrPropertyName, argTypes);
        }

        public static SyncMethod Lambda(Type parentType, string parentMethod, int lambdaOrdinal, Type[] parentArgs = null, MethodType parentMethodType = MethodType.Normal)
        {
            return Sync.RegisterSyncMethod(
                MpMethodUtil.GetLambda(parentType, parentMethod, parentMethodType, parentArgs, lambdaOrdinal),
                null
            );
        }

        public static SyncMethod LambdaInGetter(Type parentType, string parentMethod, int lambdaOrdinal)
        {
            return Sync.RegisterSyncMethod(
                MpMethodUtil.GetLambda(parentType, parentMethod, MethodType.Getter, null, lambdaOrdinal),
                null
            );
        }

        public static SyncMethod LocalFunc(Type parentType, string parentMethod, string name, Type[] parentArgs = null)
        {
            return Sync.RegisterSyncMethod(
                MpMethodUtil.GetLocalFunc(parentType, parentMethod, MethodType.Normal, parentArgs, name),
                null
            );
        }

        public override void Validate()
        {
            ValidateType("Target type", targetTransformer?.networkType ?? targetType);

            for (int i = 0; i < argTypes.Length; i++)
                ValidateType($"Arg {i} type", argTransformers[i]?.networkType ?? argTypes[i]);
        }

        public override string ToString()
        {
            return $"SyncMethod {method.MethodDesc()}";
        }
    }

}
