using LibKenjinx.Jni;
using LibKenjinx.Jni.Identifiers;
using LibKenjinx.Jni.Pointers;
using LibKenjinx.Jni.Primitives;
using LibKenjinx.Jni.References;
using LibKenjinx.Jni.Values;
using Rxmxnx.PInvoke;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Text;

namespace LibKenjinx.Android
{
    internal unsafe static class Interop
    {
        internal const string BaseClassName = "org/kenjinx/android/KenjinxNative";

        private static JGlobalRef? _classId;
        private static ConcurrentDictionary<(string method, string descriptor), JMethodId> _methodCache = new();
        private static (string name, string descriptor)[] _methods =
        [
            ("test", "()V"),
            ("updateUiHandler", "(JJJIIIIJJ)V"),
            ("frameEnded", "()V"),
            ("updateProgress", "(JF)V"),
            ("getSurfacePtr", "()J"),
            ("getWindowHandle", "()J")
        ];

        internal static void Initialize(JEnvRef jniEnv)
        {
            var vm = JniHelper.GetVirtualMachine(jniEnv);
            if (_classId == null)
            {
                var className = new ReadOnlySpan<Byte>(Encoding.UTF8.GetBytes(BaseClassName));
                using (IReadOnlyFixedMemory<Byte>.IDisposable cName = className.GetUnsafeValPtr()
                           .GetUnsafeFixedContext(className.Length))
                {
                    _classId = JniHelper.GetGlobalClass(jniEnv, cName);
                    if (_classId == null)
                    {
                        Logger.Info?.Print(LogClass.Application, $"Class Id {BaseClassName} not found");
                        return;
                    }
                }
            }

            foreach (var x in _methods)
            {
                CacheMethod(jniEnv, x.name, x.descriptor);
            }

            JniEnv._jvm = vm;
        }

        private static void CacheMethod(JEnvRef jEnv, string name, string descriptor)
        {
            if (!_methodCache.TryGetValue((name, descriptor), out var method))
            {
                var methodName = new ReadOnlySpan<Byte>(Encoding.UTF8.GetBytes(name));
                var descriptorId = new ReadOnlySpan<Byte>(Encoding.UTF8.GetBytes(descriptor));
                using (IReadOnlyFixedMemory<Byte>.IDisposable mName = methodName.GetUnsafeValPtr()
                       .GetUnsafeFixedContext(methodName.Length))
                using (IReadOnlyFixedMemory<Byte>.IDisposable dName = descriptorId.GetUnsafeValPtr()
                       .GetUnsafeFixedContext(descriptorId.Length))
                {
                    if (_classId != null)
                    {
                        var methodId = JniHelper.GetStaticMethodId(jEnv, (JClassLocalRef)(_classId.Value.Value), mName, dName);
                        if (methodId == null)
                        {
                            Logger.Warning?.Print(LogClass.Application, $"Java Method Id {name} not found");
                            return;
                        }

                        method = methodId.Value;
                    }

                    _methodCache[(name, descriptor)] = method;
                }
            }
        }

        private static void CallVoidMethod(string name, string descriptor, params JValue[] values)
        {
            using var env = JniEnv.Create();
            if (_methodCache.TryGetValue((name, descriptor), out var method))
            {
                if (descriptor.EndsWith("V"))
                {
                    if (env != null && _classId != null)
                    {
                        JniHelper.CallStaticVoidMethod(env.Env, (JClassLocalRef)(_classId.Value.Value), method, values);
                    }
                }
            }
        }

        private static JLong CallLongMethod(string name, string descriptor, params JValue[] values)
        {
            using var env = JniEnv.Create();
            if (_methodCache.TryGetValue((name, descriptor), out var method))
            {
                if (descriptor.EndsWith("J"))
                    if (env != null && _classId != null)
                    {
                        return JniHelper.CallStaticLongMethod(env.Env, (JClassLocalRef)(_classId.Value.Value), method,
                            values) ?? -1;
                    }
            }

            return -1;
        }

        public static void Test()
        {
            CallVoidMethod("test", "()V");
        }

        public static void FrameEnded(double time)
        {
            CallVoidMethod("frameEnded", "()V");
        }

        public static void UpdateProgress(string info, float progress)
        {
            using var infoPtr = new TempNativeString(info);
            CallVoidMethod("updateProgress", "(JF)V",
                JValue.Create(infoPtr.AsBytes()),
                JValue.Create(progress.AsBytes()));
        }

        public static JLong GetSurfacePtr()
        {
            return CallLongMethod("getSurfacePtr", "()J");
        }

        public static JLong GetWindowsHandle()
        {
            return CallLongMethod("getWindowHandle", "()J");
        }

        public static void UpdateUiHandler(string newTitle,
            string newMessage,
            string newWatermark,
            int newType,
            int min,
            int max,
            KeyboardMode nMode,
            string newSubtitle,
            string newInitialText)
        {
            using var titlePointer = new TempNativeString(newTitle);
            using var messagePointer = new TempNativeString(newMessage);
            using var watermarkPointer = new TempNativeString(newWatermark);
            using var subtitlePointer = new TempNativeString(newSubtitle);
            using var newInitialPointer = new TempNativeString(newInitialText);
            CallVoidMethod("updateUiHandler", "(JJJIIIIJJ)V",
                JValue.Create(titlePointer.AsBytes()),
                JValue.Create(messagePointer.AsBytes()),
                JValue.Create(watermarkPointer.AsBytes()),
                JValue.Create(newType.AsBytes()),
                JValue.Create(min.AsBytes()),
                JValue.Create(max.AsBytes()),
                JValue.Create(nMode.AsBytes()),
                JValue.Create(subtitlePointer.AsBytes()),
                JValue.Create(newInitialPointer.AsBytes()));
        }

        private class TempNativeString : IDisposable
        {
            private JLong _jPointer;

            public TempNativeString(string value)
            {
                Pointer = Marshal.StringToHGlobalAuto(value);
                JPointer = Pointer;
            }

            public nint Pointer { get; private set; }
            public JLong JPointer { get => _jPointer; private set => _jPointer = value; }

            public Span<byte> AsBytes()
            {
                return _jPointer.AsBytes();
            }

            public void Dispose()
            {
                if (Pointer != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(Pointer);
                }
                Pointer = IntPtr.Zero;
            }
        }

        private class JniEnv : IDisposable
        {
            internal static JavaVMRef? _jvm;
            private readonly JEnvRef _env;
            private readonly bool _newAttach;

            public JEnvRef Env => _env;

            private JniEnv(JEnvRef env, bool newAttach)
            {
                _env = env;
                _newAttach = newAttach;
            }

            public void Dispose()
            {
                if(_newAttach)
                {
                    JniHelper.Detach(_jvm!.Value);
                }
            }

            public static JniEnv? Create()
            {
                bool newAttach = false;
                ReadOnlySpan<Byte> threadName = "JvmCall"u8;
                var env = _jvm == null ? default : JniHelper.Attach(_jvm.Value, threadName.GetUnsafeValPtr().GetUnsafeFixedContext(threadName.Length),
                                         out newAttach);

                return env != null ? new JniEnv(env.Value, newAttach) : null;
            }
        }
    }
}
