#pragma once
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <TlHelp32.h>
#include <vector>

struct Call {
  int returnValue;
  std::string returnString;
  DWORD exitCode;
};

namespace functions {
  enum class Type {
    T_VOID = 0x0,
    T_STRING = 0x1,
    T_CHAR = 0x2,
    T_BOOL = 0x3,
    T_INT = 0x4,
    T_DOUBLE = 0x5,
    T_FLOAT = 0x6
  };

  struct Arg {
    Type type;
    LPVOID value;
  };

  LPVOID reserveString(HANDLE hProcess, const char* value, SIZE_T size);
  char readChar(HANDLE hProcess, DWORD64 address);

  template <class returnDataType>
  Call call(HANDLE pHandle, std::vector<Arg> args, Type returnType, DWORD64 address, char** errorMessage) {
    std::vector<unsigned char> argShellcode;

    std::reverse(args.begin(), args.end());

    for (auto &arg : args) {
      // 0x68: PUSH imm16/imm32
      // 0x6A: PUSH imm8

      if (arg.type == Type::T_INT || arg.type == Type::T_FLOAT) {
        argShellcode.push_back(0x68);
        int value = *static_cast<int*>(arg.value);

        // Little endian representation
        for (int i = 0; i < 4; i++) {
          int shifted = (value >> (i * 8)) & 0xFF;
          argShellcode.push_back(shifted);
        }

        continue;
      }

      if (arg.type == Type::T_STRING) {
        argShellcode.push_back(0x68);
        std::string value = *static_cast<std::string*>(arg.value);
        LPVOID address = functions::reserveString(pHandle, value.c_str(), value.length());

        // Little endian representation
        for (int i = 0; i < 4; i++) {
          int shifted = ((int)address >> (i * 8)) & 0xFF;
          argShellcode.push_back(shifted);
        }

        continue;
      }

      argShellcode.push_back(0x6A);
      unsigned char value = *static_cast<unsigned char*>(arg.value);
      argShellcode.push_back(value);
    }

    // 83: ADD r/m16/32 imm8
    std::vector<unsigned char> callShellcode = {
      // call 0x00000000
      0xE8, 0x00, 0x00, 0x00, 0x00,
      // add esp, [arg count * 4]
      0x83, 0xC4, (unsigned char)(args.size() * 0x4),
    };

    LPVOID returnValuePointer = 0;
    if (returnType != Type::T_VOID) {
      // We will reserve memory for where we want to store the result,
      // and move the return value to this address.
      returnValuePointer = VirtualAllocEx(pHandle, NULL, sizeof(returnDataType), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

      if (returnType == Type::T_FLOAT) {
        // fstp DWORD PTR [0x12345678]
        // when `call` is executed, if return type is float, it's stored
        // in fpu registers (st0 through st7). we can use the `fst`
        // instruction to move st0 to a place in memory
        // D9 FSTP m32real ST Store Floating Point Value and Pop
        // D9 = for m32
        callShellcode.push_back(0xD9);
        callShellcode.push_back(0x1C);
        callShellcode.push_back(0x25);
      } else if (returnType == Type::T_DOUBLE) {
        // fstp QWORD PTR [0x12345678]
        // DD FSTP m64real ST Store Floating Point Value and Pop
        // DD = for m64
        callShellcode.push_back(0xDD);
        callShellcode.push_back(0x1C);
        callShellcode.push_back(0x25);
      } else {
        // mov [0x1234], eax
        // A3: MOVE moffs16/32 eAX
        // Call routines places return value inside EAX
        callShellcode.push_back(0xA3);
      }

      for (int i = 0; i < 4; i++) {
        int shifted = ((DWORD)returnValuePointer >> (i * 8)) & 0xFF;
        callShellcode.push_back(shifted);
      }
    }

    // C3: return
    callShellcode.push_back(0xC3);

    // concatenate the arg shellcode with the calling shellcode
    std::vector<unsigned char> shellcode;
    shellcode.reserve(argShellcode.size() + callShellcode.size());
    shellcode.insert(shellcode.end(), argShellcode.begin(), argShellcode.end());
    shellcode.insert(shellcode.end(), callShellcode.begin(), callShellcode.end());

    // 5 = 0xE8 (call) + 4 empty bytes for where the address will go
    int addessShellcodeOffset = argShellcode.size() + 5;

    // Allocate space for the shellcode
    SIZE_T size = shellcode.size() * sizeof(unsigned char);
    LPVOID pShellcode = VirtualAllocEx(pHandle, NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

    // `call` opcode takes relative address, so calculate the relative address
    // taking into account where the shellcode will be written in memory
    DWORD64 relative = address - (uintptr_t)pShellcode - addessShellcodeOffset;

    // Write the relative address to the shellcode
    for (int i = 0; i < 4; i++) {
      int shifted = (relative >> (i * 8)) & 0xFF;

      // argShellcode.size() will offset to the `0xE8` opcode, add 1 to offset that instruction
      shellcode.at(argShellcode.size() + 1 + i) = shifted;
    }

    // Write the shellcode
    WriteProcessMemory(pHandle, pShellcode, shellcode.data(), size, NULL);

    // Execute the shellcode
    HANDLE thread = CreateRemoteThread(pHandle, NULL, NULL, (LPTHREAD_START_ROUTINE)pShellcode, NULL, NULL, NULL);

    Call data = { 0, "", (DWORD) -1 };

    if (thread == NULL) {
      *errorMessage = "unable to create remote thread.";
      return data;
    }

    WaitForSingleObject(thread, INFINITE);
    GetExitCodeThread(thread, &data.exitCode);

    if (returnType != Type::T_VOID && returnType != Type::T_STRING) {
      ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &data.returnValue, sizeof(int), NULL);
      VirtualFreeEx(pHandle, returnValuePointer, 0, MEM_RELEASE);
    }

    if (returnType == Type::T_STRING) {
      // String is stored in memory somewhere
      // When returning a string, the address of the string is placed in EAX.
      // So we read the current returnValuePointer address to get the actual address of the string
      ReadProcessMemory(pHandle, (LPVOID)returnValuePointer, &returnValuePointer, sizeof(int), NULL);

      std::vector<char> chars;
      int offset = 0x0;
      while (true) {
        char c = functions::readChar(pHandle, (DWORD64)returnValuePointer + offset);
        chars.push_back(c);

        // break at 1 million chars
        if (offset == (sizeof(char) * 1000000)) {
          chars.clear();
          break;
        }

        // break at terminator (end of string)
        if (c == '\0') {
          break;
        }

        // go to next char
        offset += sizeof(char);
      }

      std::string str(chars.begin(), chars.end());
      // TODO: pass str as LPVOID and cast back to string
      data.returnString = str;
    }

    VirtualFreeEx(pHandle, pShellcode, 0, MEM_RELEASE);

    return data;
  }
}

#endif
#pragma once
