// modules
using System.Runtime.InteropServices;

namespace ProcessWrapper {
    public interface IProcessWrapper {
        byte[] ReadProcessMemory(int pid, ulong address, uint length);
        bool WriteProcessMemory(int pid, ulong address, byte[] data);
        int getuid();
        ulong FindCemuBase(int pid, ulong minSize);
    }

    // Mac implementation
    public class MacProcessWrapper : IProcessWrapper {
        // vars
        const int KERN_SUCCESS = 0;
        const string LIBSYSTEM = "/usr/lib/libSystem.B.dylib";
        const int VM_REGION_BASIC_INFO_64 = 9;
        const int VM_REGION_BASIC_INFO_COUNT_64 = 10;
        const int VM_PROT_READ = 0x01;

        // Struct
        [StructLayout(LayoutKind.Sequential)]
        public struct vm_region_basic_info_64 {
            public int protection;
            public int max_protection;
            public int inheritance;
            public int shared;
            public int reserved;
            public ulong offset;
            public int behavior;
            public short user_wired_count;
            public short user_tag;
        }

        // https://github.com/attilathedud/macos_task_for_pid#overview
        // Well documenated apis, thanks Apple.
        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int task_for_pid(IntPtr task,
            int pid,
            out IntPtr targetTask);

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern IntPtr mach_task_self();

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int mach_vm_read(IntPtr target_task,
            ulong address,
            ulong size,
            out IntPtr data,
            out ulong data_count);

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int mach_vm_write(IntPtr target_task,
            ulong address,
            byte[] data,
            uint dataCnt);

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int vm_deallocate(IntPtr task,
            IntPtr address,
            ulong size);

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int mach_vm_region(IntPtr task,
            ref ulong address,
            out ulong size,
            int flavor,
            IntPtr info,
            ref uint count,
            out ulong object_name);

        [DllImport(LIBSYSTEM, SetLastError = true)]
        private static extern int geteuid();

        public int getuid() {
            return geteuid();
        }

        public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
            IntPtr task;
            IntPtr localTask = mach_task_self();

            int result = task_for_pid(localTask, pid, out task);
            if (result != KERN_SUCCESS) {
                Console.WriteLine($"task_for_pid failed with code {result}");
                return new byte[0];
            }

            IntPtr bufferPtr;
            ulong size;
            result = mach_vm_read(task, address, length, out bufferPtr, out size);
            if (result != KERN_SUCCESS) {
                //Console.WriteLine($"mach_vm_read failed with code {result}");
                return new byte[0];
            }

            byte[] buffer = new byte[size];
            Marshal.Copy(bufferPtr, buffer, 0, (int)size);

            // Clean up
            vm_deallocate(localTask, bufferPtr, size);

            return buffer;
        }

        public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
            IntPtr task;
            IntPtr localTask = mach_task_self();

            int result = task_for_pid(localTask, pid, out task);
            if (result != KERN_SUCCESS) {
                //throw new Exception($"task_for_pid failed: {result}");
                return false;
            }

            result = mach_vm_write(task, address, data, (uint)data.Length);
            if (result != KERN_SUCCESS) {
                //throw new Exception($"mach_vm_write failed: {result}");
                Console.WriteLine($"mach_vm_write failed: {result}");
                return false;
            } else {
                return true;
            }

            // Console.WriteLine($"Wrote {data.Length} bytes to 0x{address:X}");
        }

        public ulong FindCemuBase(int pid, ulong maxSize) {
            IntPtr task;
            IntPtr localTask = mach_task_self();
            byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };

            int result = task_for_pid(localTask, pid, out task);
            if (result != KERN_SUCCESS) {
                Console.WriteLine($"task_for_pid failed: {result}");
                return 0;
            }

            ulong address = 0;
            while (true) {
                ulong size;
                uint count = VM_REGION_BASIC_INFO_COUNT_64;
                IntPtr infoPtr = Marshal.AllocHGlobal(Marshal.SizeOf<vm_region_basic_info_64>());
                ulong objectName;

                result = mach_vm_region(task,
                    ref address,
                    out size,
                    VM_REGION_BASIC_INFO_64,
                    infoPtr,
                    ref count,
                    out objectName
                );

                if (result != KERN_SUCCESS) {
                    Marshal.FreeHGlobal(infoPtr);
                    break;
                }

                var regionInfo = Marshal.PtrToStructure<vm_region_basic_info_64>(infoPtr);
                Marshal.FreeHGlobal(infoPtr);

                bool readable = (regionInfo.protection & VM_PROT_READ) != 0;

                if (readable && size < maxSize || size == maxSize) {
                    var bytes = ReadProcessMemory(pid, (ulong)(address + 0xE000000), 20);

                    if (bytes != null && bytes.Length > 0) {
                        int patternLen = patternBytes.Length;
                        for (int i = 0, j = 0; i + patternLen <= 20; i++) {
                            if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
                                j++;
                            } else {
                                j = 0;
                            }

                            if (j >= patternLen) {
                                return address;
                            }
                        }
                    }
                }

                address += size;
            }
            return 0;
        }
    }

    // Linux implementation
    public class LinuxProcessWrapper : IProcessWrapper {
        // IOVec Struct
        [StructLayout(LayoutKind.Sequential)]
        private struct IOVec {
            public UIntPtr Base;
            public UIntPtr Length;
        }

        [DllImport("libc", SetLastError = true)]
        private static extern long process_vm_readv(int pid,
            IOVec[] local_iov, ulong liovcnt,
            IOVec[] remote_iov, ulong riovcnt,
            ulong flags);

        [DllImport("libc", SetLastError = true)]
        private static extern long process_vm_writev(int pid,
            IOVec[] local_iov, ulong liovcnt,
            IOVec[] remote_iov, ulong riovcnt,
            ulong flags);

        [DllImport("libc", SetLastError = true)]
        private static extern int geteuid();

        public int getuid() {
            return geteuid();
        }

        public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
            byte[] buffer = new byte[length];
            GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

            try {
                var local = new IOVec {
                    Base = new UIntPtr((ulong)handle.AddrOfPinnedObject().ToInt64()),
                    Length = (UIntPtr)length
                };

                var remote = new IOVec {
                    Base = (UIntPtr)address,
                    Length = (UIntPtr)length
                };

                long nread = process_vm_readv(pid, new[] { local }, 1, new[] { remote }, 1, 0);
                if (nread == -1) {
                    // Console.WriteLine($"Failed to read memory. Error code: {Marshal.GetLastWin32Error()}");
                    return new byte[0];
                } else {
                    // Console.WriteLine($"Read {nread} bytes from process {pid} at 0x{address:X}");
                }
            } finally {
                handle.Free();
            }

            return buffer;
        }

        public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
            GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);

            try {
                var local = new IOVec {
                    Base = new UIntPtr((ulong)handle.AddrOfPinnedObject().ToInt64()),
                    Length = (UIntPtr)data.Length
                };

                var remote = new IOVec {
                    Base = (UIntPtr)address,
                    Length = (UIntPtr)data.Length
                };

                long nwritten = process_vm_writev(pid, new[] { local }, 1, new[] { remote }, 1, 0);
                if (nwritten == -1) {
                    // Console.WriteLine($"Failed to write memory. Error code: {Marshal.GetLastWin32Error()}");
                    return false;
                } else {
                    // Console.WriteLine($"Wrote {nwritten} bytes to process {pid} at 0x{address:X}");
                    return true;
                }
            }
            finally {
                handle.Free();
            }
        }

        public ulong FindCemuBase(int pid, ulong maxSize) {
            // vars
            string mapsPath = $"/proc/{pid}/maps";
            byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };

            string[] mapsLines = File.ReadAllLines(mapsPath);
            foreach (string line in mapsLines) {
                var split = line.Split(" ");
                var addrP = split[0].Split("-");
                ulong min = Convert.ToUInt64(addrP[0], 16);
                ulong max = Convert.ToUInt64(addrP[1], 16);

                if (!split[1].Contains("r")) continue;
                var len = (max - min);
                if (len > maxSize || len == maxSize) {
                  var bytes = ReadProcessMemory(pid, (ulong)(min + 0xE000000), 20);

                  if (bytes != null && bytes.Length > 0) {
                      int patternLen = patternBytes.Length;
                      for (int i = 0, j = 0; i + patternLen <= 20; i++) {
                          if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
                              j++;
                          } else {
                              j = 0;
                          }

                          if (j >= patternLen) {
                            return min;
                          }
                      }
                  }
               }
            }
            return 0;
        }
    }

    public class WindowsProcessWrapper : IProcessWrapper {
        // vars
        const uint PROCESS_VM_READ = 0x0010;
        const uint PROCESS_VM_WRITE = 0x0020;
        const uint PROCESS_VM_OPERATION = 0x0008;
        const uint PROCESS_QUERY_INFORMATION = 0x0400;

        [StructLayout(LayoutKind.Sequential)]
            public struct MEMORY_BASIC_INFORMATION {
            public IntPtr BaseAddress;
            public IntPtr AllocationBase;
            public uint AllocationProtect;
            public IntPtr RegionSize;
            public uint State;
            public uint Protect;
            public uint Type;
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr OpenProcess(uint dwDesiredAccess,
            bool bInheritHandle,
            int dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool ReadProcessMemory(IntPtr hProcess,
            IntPtr lpBaseAddress,
            [Out] byte[] lpBuffer,
            int dwSize,
            out int lpNumberOfBytesRead);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool WriteProcessMemory(IntPtr hProcess,
            IntPtr lpBaseAddress,
            byte[] lpBuffer,
            int dwSize,
            out int lpNumberOfBytesWritten);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern int VirtualQueryEx(IntPtr hProcess,
            IntPtr lpAddress,
            out MEMORY_BASIC_INFORMATION lpBuffer,
            uint dwLength);

        // Windows doesnt need this.
        public int getuid() {
            return -1;
        }

        public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
            IntPtr hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
            if (hProcess == IntPtr.Zero) {
                return new byte[0];
            }

            byte[] buffer = new byte[length];
            if (ReadProcessMemory(hProcess, (IntPtr)address, buffer, (int)length, out int bytesRead) && bytesRead > 0) {
                if (bytesRead < length) {
                    Array.Resize(ref buffer, bytesRead);
                }
                return buffer;
            }

            return new byte[0];
        }

        public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
            IntPtr hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false, pid);
            if (hProcess == IntPtr.Zero) {
                return false;
            }

            return WriteProcessMemory(hProcess, (IntPtr)address, data, data.Length, out int bytesWritten) && bytesWritten == data.Length;
        }

        public ulong FindCemuBase(int pid, ulong maxSize) {
            byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };
            IntPtr address = IntPtr.Zero;
            MEMORY_BASIC_INFORMATION mbi;

            IntPtr hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false, pid);
            if (hProcess == IntPtr.Zero) {
                return 0;
            }

            while (VirtualQueryEx(hProcess, address, out mbi, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) != 0) {
                bool readable = (mbi.State == 0x1000) && (mbi.Protect != 0x01);
                ulong regionSize = (ulong)mbi.RegionSize.ToInt64();
                if (readable && regionSize > maxSize || regionSize == maxSize) {
                    var bytes = ReadProcessMemory(pid, (ulong)(mbi.BaseAddress + 0xE000000), 20);
                    if (bytes != null && bytes.Length > 0) {
                        int patternLen = patternBytes.Length;
                        for (int i = 0, j = 0; i + patternLen <= 20; i++) {
                            if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
                                j++;
                            } else {
                                j = 0;
                            }
                            if (j >= patternLen) {
                                return (ulong)mbi.BaseAddress;
                            }
                        }
                    }
                }
                address = new IntPtr(mbi.BaseAddress.ToInt64() + mbi.RegionSize.ToInt64());
            }
            return 0;
        }
    }
}
