#include "linker.hpp"
#include "io-device.hpp"
#include "orbis/KernelAllocator.hpp"
#include "orbis/module/Module.hpp"
#include "orbis/stat.hpp"
#include "orbis/uio.hpp"
#include "vfs.hpp"
#include "vm.hpp"
#include <bit>
#include <crypto/sha1.h>
#include <elf.h>
#include <filesystem>
#include <map>
#include <orbis/thread/Process.hpp>
#include <rx/align.hpp>
#include <sys/mman.h>
#include <unordered_map>

using orbis::utils::Ref;

std::uint64_t monoPimpAddress;

static std::vector<std::byte> unself(const std::byte *image, std::size_t size) {
  struct [[gnu::packed]] Header {
    std::uint32_t magic;
    std::uint32_t unk;
    std::uint8_t category;
    std::uint8_t programType;
    std::uint16_t padding;
    std::uint16_t headerSize;
    std::uint16_t signSize;
    std::uint32_t fileSize;
    std::uint32_t padding2;
    std::uint16_t segmentCount;
    std::uint16_t unk1;
    std::uint32_t padding3;
  };
  static_assert(sizeof(Header) == 0x20);

  struct [[gnu::packed]] Segment {
    std::uint64_t flags;
    std::uint64_t offset;
    std::uint64_t encryptedSize;
    std::uint64_t decryptedSize;
  };
  static_assert(sizeof(Segment) == 0x20);

  auto header = std::bit_cast<Header *>(image);
  auto segments = std::bit_cast<Segment *>(image + sizeof(Header));

  auto elfOffset = sizeof(Header) + sizeof(Segment) * header->segmentCount;
  std::vector<std::byte> result;
  result.reserve(header->fileSize);
  result.resize(sizeof(Elf64_Ehdr));

  auto ehdr = std::bit_cast<Elf64_Ehdr *>(image + elfOffset);
  auto phdrs = std::bit_cast<Elf64_Phdr *>(image + elfOffset + ehdr->e_phoff);
  std::memcpy(result.data(), ehdr, sizeof(Elf64_Ehdr));

  auto phdrEndOffset = ehdr->e_phoff + ehdr->e_phentsize * ehdr->e_phnum;
  if (result.size() < phdrEndOffset) {
    result.resize(phdrEndOffset);
  }

  for (std::size_t i = 0; i < ehdr->e_phnum; ++i) {
    std::memcpy(result.data() + ehdr->e_phoff + ehdr->e_phentsize * i,
                image + elfOffset + ehdr->e_phoff + ehdr->e_phentsize * i,
                sizeof(Elf64_Phdr));
  }

  for (std::size_t i = 0; i < header->segmentCount; ++i) {
    auto &segment = segments[i];
    if ((segment.flags & 0x7fb) != 0 ||
        segment.decryptedSize != segment.encryptedSize) {
      std::fprintf(stderr, "Unsupported self segment (%lx)\n", segment.flags);
      std::abort();
    }

    if (~segment.flags & 0x800) {
      continue;
    }

    auto &phdr = phdrs[segment.flags >> 20];

    auto endOffset = phdr.p_offset + segment.decryptedSize;
    if (result.size() < endOffset) {
      result.resize(endOffset);
    }

    std::memcpy(result.data() + phdr.p_offset, image + segment.offset,
                segment.decryptedSize);
  }

  return result;
}

std::uint64_t rx::linker::encodeFid(std::string_view fid) {
  static const char suffix[] =
      "\x51\x8D\x64\xA6\x35\xDE\xD8\xC1\xE6\xB0\x39\xB1\xC3\xE5\x52\x30";

  sha1_context ctx;
  unsigned char output[20];

  sha1_starts(&ctx);
  sha1_update(&ctx, reinterpret_cast<const unsigned char *>(fid.data()),
              fid.length());
  sha1_update(&ctx, reinterpret_cast<const unsigned char *>(suffix),
              sizeof(suffix) - 1);
  sha1_finish(&ctx, output);

  std::uint64_t hash;
  std::memcpy(&hash, output, sizeof(hash));
  return hash;
}

enum OrbisElfProgramType {
  kElfProgramTypeNull = 0,
  kElfProgramTypeLoad = 1,
  kElfProgramTypeDynamic = 2,
  kElfProgramTypeInterp = 3,
  kElfProgramTypeNote = 4,
  kElfProgramTypeShlib = 5,
  kElfProgramTypePhdr = 6,
  kElfProgramTypeTls = 7,
  kElfProgramTypeSceDynlibData = 0x61000000,
  kElfProgramTypeSceProcParam = 0x61000001,
  kElfProgramTypeSceModuleParam = 0x61000002,
  kElfProgramTypeSceRelRo = 0x61000010,
  kElfProgramTypeGnuEhFrame = 0x6474e550,
  kElfProgramTypeGnuRelRo = 0x6474e552,
  kElfProgramTypeSceComment = 0x6fffff00,
  kElfProgramTypeSceVersion = 0x6fffff01,
};

enum OrbisElfDynamicType {
  kElfDynamicTypeNull = 0,
  kElfDynamicTypeNeeded = 1,
  kElfDynamicTypePltRelSize = 2,
  kElfDynamicTypePltGot = 3,
  kElfDynamicTypeHash = 4,
  kElfDynamicTypeStrTab = 5,
  kElfDynamicTypeSymTab = 6,
  kElfDynamicTypeRela = 7,
  kElfDynamicTypeRelaSize = 8,
  kElfDynamicTypeRelaEnt = 9,
  kElfDynamicTypeStrSize = 10,
  kElfDynamicTypeSymEnt = 11,
  kElfDynamicTypeInit = 12,
  kElfDynamicTypeFini = 13,
  kElfDynamicTypeSoName = 14,
  kElfDynamicTypeRpath = 15,
  kElfDynamicTypeSymbolic = 16,
  kElfDynamicTypeRel = 17,
  kElfDynamicTypeRelSize = 18,
  kElfDynamicTypeRelEnt = 19,
  kElfDynamicTypePltRel = 20,
  kElfDynamicTypeDebug = 21,
  kElfDynamicTypeTextRel = 22,
  kElfDynamicTypeJmpRel = 23,
  kElfDynamicTypeBindNow = 24,
  kElfDynamicTypeInitArray = 25,
  kElfDynamicTypeFiniArray = 26,
  kElfDynamicTypeInitArraySize = 27,
  kElfDynamicTypeFiniArraySize = 28,
  kElfDynamicTypeRunPath = 29,
  kElfDynamicTypeFlags = 30,
  kElfDynamicTypePreinitArray = 32,
  kElfDynamicTypePreinitArraySize = 33,
  kElfDynamicTypeSceFingerprint = 0x61000007,
  kElfDynamicTypeSceOriginalFilename = 0x61000009,
  kElfDynamicTypeSceModuleInfo = 0x6100000d,
  kElfDynamicTypeSceNeededModule = 0x6100000f,
  kElfDynamicTypeSceModuleAttr = 0x61000011,
  kElfDynamicTypeSceExportLib = 0x61000013,
  kElfDynamicTypeSceImportLib = 0x61000015,
  kElfDynamicTypeSceExportLibAttr = 0x61000017,
  kElfDynamicTypeSceImportLibAttr = 0x61000019,
  kElfDynamicTypeSceHash = 0x61000025,
  kElfDynamicTypeScePltGot = 0x61000027,
  kElfDynamicTypeSceJmpRel = 0x61000029,
  kElfDynamicTypeScePltRel = 0x6100002b,
  kElfDynamicTypeScePltRelSize = 0x6100002d,
  kElfDynamicTypeSceRela = 0x6100002f,
  kElfDynamicTypeSceRelaSize = 0x61000031,
  kElfDynamicTypeSceRelaEnt = 0x61000033,
  kElfDynamicTypeSceStrTab = 0x61000035,
  kElfDynamicTypeSceStrSize = 0x61000037,
  kElfDynamicTypeSceSymTab = 0x61000039,
  kElfDynamicTypeSceSymEnt = 0x6100003b,
  kElfDynamicTypeSceHashSize = 0x6100003d,
  kElfDynamicTypeSceOriginalFilename1 = 0x61000041,
  kElfDynamicTypeSceModuleInfo1 = 0x61000043,
  kElfDynamicTypeSceNeededModule1 = 0x61000045,
  kElfDynamicTypeSceExportLib1 = 0x61000047,
  kElfDynamicTypeSceImportLib1 = 0x61000049,
  kElfDynamicTypeSceSymTabSize = 0x6100003f,
  kElfDynamicTypeRelaCount = 0x6ffffff9
};

inline const char *toString(OrbisElfDynamicType dynType) {
  switch (dynType) {
  case kElfDynamicTypeNull:
    return "Null";
  case kElfDynamicTypeNeeded:
    return "Needed";
  case kElfDynamicTypePltRelSize:
    return "PltRelSize";
  case kElfDynamicTypePltGot:
    return "PltGot";
  case kElfDynamicTypeHash:
    return "Hash";
  case kElfDynamicTypeStrTab:
    return "StrTab";
  case kElfDynamicTypeSymTab:
    return "SymTab";
  case kElfDynamicTypeRela:
    return "Rela";
  case kElfDynamicTypeRelaSize:
    return "RelaSize";
  case kElfDynamicTypeRelaEnt:
    return "RelaEnt";
  case kElfDynamicTypeStrSize:
    return "StrSize";
  case kElfDynamicTypeSymEnt:
    return "SymEnt";
  case kElfDynamicTypeInit:
    return "Init";
  case kElfDynamicTypeFini:
    return "Fini";
  case kElfDynamicTypeSoName:
    return "SoName";
  case kElfDynamicTypeRpath:
    return "Rpath";
  case kElfDynamicTypeSymbolic:
    return "Symbolic";
  case kElfDynamicTypeRel:
    return "Rel";
  case kElfDynamicTypeRelSize:
    return "RelSize";
  case kElfDynamicTypeRelEnt:
    return "RelEnt";
  case kElfDynamicTypePltRel:
    return "PltRel";
  case kElfDynamicTypeDebug:
    return "Debug";
  case kElfDynamicTypeTextRel:
    return "TextRel";
  case kElfDynamicTypeJmpRel:
    return "JmpRel";
  case kElfDynamicTypeBindNow:
    return "BindNow";
  case kElfDynamicTypeInitArray:
    return "InitArray";
  case kElfDynamicTypeFiniArray:
    return "FiniArray";
  case kElfDynamicTypeInitArraySize:
    return "InitArraySize";
  case kElfDynamicTypeFiniArraySize:
    return "FiniArraySize";
  case kElfDynamicTypeRunPath:
    return "RunPath";
  case kElfDynamicTypeFlags:
    return "Flags";
  case kElfDynamicTypePreinitArray:
    return "PreinitArray";
  case kElfDynamicTypePreinitArraySize:
    return "PreinitArraySize";
  case kElfDynamicTypeSceFingerprint:
    return "SceFingerprint";
  case kElfDynamicTypeSceOriginalFilename:
    return "SceOriginalFilename";
  case kElfDynamicTypeSceModuleInfo:
    return "SceModuleInfo";
  case kElfDynamicTypeSceNeededModule:
    return "SceNeededModule";
  case kElfDynamicTypeSceModuleAttr:
    return "SceModuleAttr";
  case kElfDynamicTypeSceExportLib:
    return "SceExportLib";
  case kElfDynamicTypeSceImportLib:
    return "SceImportLib";
  case kElfDynamicTypeSceExportLibAttr:
    return "SceExportLibAttr";
  case kElfDynamicTypeSceImportLibAttr:
    return "SceImportLibAttr";
  case kElfDynamicTypeSceHash:
    return "SceHash";
  case kElfDynamicTypeScePltGot:
    return "ScePltGot";
  case kElfDynamicTypeSceJmpRel:
    return "SceJmpRel";
  case kElfDynamicTypeScePltRel:
    return "ScePltRel";
  case kElfDynamicTypeScePltRelSize:
    return "ScePltRelSize";
  case kElfDynamicTypeSceRela:
    return "SceRela";
  case kElfDynamicTypeSceRelaSize:
    return "SceRelaSize";
  case kElfDynamicTypeSceRelaEnt:
    return "SceRelaEnt";
  case kElfDynamicTypeSceStrTab:
    return "SceStrTab";
  case kElfDynamicTypeSceStrSize:
    return "SceStrSize";
  case kElfDynamicTypeSceSymTab:
    return "SceSymTab";
  case kElfDynamicTypeSceSymEnt:
    return "SceSymEnt";
  case kElfDynamicTypeSceHashSize:
    return "SceHashSize";
  case kElfDynamicTypeSceOriginalFilename1:
    return "SceOriginalFilename1";
  case kElfDynamicTypeSceModuleInfo1:
    return "SceModuleInfo1";
  case kElfDynamicTypeSceNeededModule1:
    return "SceNeededModule1";
  case kElfDynamicTypeSceImportLib1:
    return "SceImportLib1";
  case kElfDynamicTypeSceSymTabSize:
    return "SceSymTabSize";
  case kElfDynamicTypeRelaCount:
    return "RelaCount";
  case kElfDynamicTypeSceExportLib1:
    return "SceExportLib1";
  }

  return "<unknown>";
}

struct SceProcessParam {
  std::uint64_t size = 0x40;
  std::uint32_t magic = 0x4942524F;
  std::uint32_t entryCount = 3;
  std::uint64_t sdkVersion = 0x4508101;

  std::uint64_t unk0 = 0;
  std::uint64_t unk1 = 0;
  std::uint64_t unk2 = 0;
  std::uint64_t unk3 = 0;

  std::uint64_t sceLibcParam_ptr = 0;

  // ext, size == 0x50
  std::uint64_t sceLibcKernelMemParam_ptr = 0;
  std::uint64_t sceLibcKernelFsParam_ptr = 0;
};

struct Symbol {
  orbis::Module *module;
  std::uint64_t address;
  std::uint64_t bindMode;
};

static std::map<std::string, std::filesystem::path, std::less<>>
    g_moduleOverrideTable;

void rx::linker::override(std::string originalModuleName,
                          std::filesystem::path replacedModulePath) {
  g_moduleOverrideTable[std::move(originalModuleName)] =
      std::move(replacedModulePath);
}

Ref<orbis::Module> rx::linker::loadModule(std::span<std::byte> image,
                                          orbis::Process *process) {
  Ref<orbis::Module> result{orbis::knew<orbis::Module>()};

  Elf64_Ehdr header;
  std::memcpy(&header, image.data(), sizeof(Elf64_Ehdr));
  result->type = header.e_type;

  Elf64_Phdr phdrsStorage[16];
  if (header.e_phnum > std::size(phdrsStorage)) {
    std::abort();
  }

  std::memcpy(phdrsStorage, image.data() + header.e_phoff,
              header.e_phnum * sizeof(Elf64_Phdr));
  auto phdrs = std::span(phdrsStorage, header.e_phnum);

  std::uint64_t endAddress = 0;
  std::uint64_t baseAddress = ~static_cast<std::uint64_t>(0);
  int dynamicPhdrIndex = -1;
  int interpPhdrIndex = -1;
  int notePhdrIndex = -1;
  int shlibPhdrIndex = -1;
  int phdrPhdrIndex = -1;
  int tlsPhdrIndex = -1;
  int sceDynlibDataPhdrIndex = -1;
  int sceProcParamIndex = -1;
  int sceModuleParamIndex = -1;
  int sceRelRoPhdrIndex = -1;
  int gnuEhFramePhdrIndex = -1;
  int gnuRelRoPhdrIndex = -1;
  int sceCommentPhdrIndex = -1;
  int sceVersionPhdrIndex = -1;

  for (auto &phdr : phdrs) {
    std::size_t index = &phdr - phdrs.data();

    switch (phdr.p_type) {
    case kElfProgramTypeNull:
      break;
    case kElfProgramTypeLoad:
      baseAddress =
          std::min(baseAddress, rx::alignDown(phdr.p_vaddr, phdr.p_align));
      endAddress = std::max(
          endAddress, rx::alignUp(phdr.p_vaddr + phdr.p_memsz, vm::kPageSize));
      break;
    case kElfProgramTypeDynamic:
      dynamicPhdrIndex = index;
      break;
    case kElfProgramTypeInterp:
      interpPhdrIndex = index;
      break;
    case kElfProgramTypeNote:
      notePhdrIndex = index;
      break;
    case kElfProgramTypeShlib:
      shlibPhdrIndex = index;
      break;
    case kElfProgramTypePhdr:
      phdrPhdrIndex = index;
      break;
    case kElfProgramTypeTls:
      tlsPhdrIndex = index;
      break;
    case kElfProgramTypeSceDynlibData:
      sceDynlibDataPhdrIndex = index;
      break;
    case kElfProgramTypeSceProcParam:
      sceProcParamIndex = index;
      break;
    case kElfProgramTypeSceModuleParam:
      sceModuleParamIndex = index;
      break;
    case kElfProgramTypeSceRelRo:
      sceRelRoPhdrIndex = index;
      baseAddress =
          std::min(baseAddress, rx::alignDown(phdr.p_vaddr, phdr.p_align));
      endAddress = std::max(
          endAddress, rx::alignUp(phdr.p_vaddr + phdr.p_memsz, vm::kPageSize));
      break;
    case kElfProgramTypeGnuEhFrame:
      gnuEhFramePhdrIndex = index;
      break;
    case kElfProgramTypeGnuRelRo:
      gnuRelRoPhdrIndex = index;
      break;
    case kElfProgramTypeSceComment:
      sceCommentPhdrIndex = index;
      break;
    case kElfProgramTypeSceVersion:
      sceVersionPhdrIndex = index;
      break;
    }
  }

  auto imageSize = endAddress - baseAddress;

  auto imageBase = reinterpret_cast<std::byte *>(
      vm::map(reinterpret_cast<void *>(baseAddress),
              rx::alignUp(imageSize, vm::kPageSize), 0,
              vm::kMapFlagPrivate | vm::kMapFlagAnonymous |
                  (baseAddress ? vm::kMapFlagFixed : 0)));

  if (imageBase == MAP_FAILED) {
    std::abort();
  }

  result->entryPoint = header.e_entry
                           ? reinterpret_cast<std::uintptr_t>(
                                 imageBase - baseAddress + header.e_entry)
                           : 0;

  if (interpPhdrIndex >= 0) {
    result->interp = reinterpret_cast<const char *>(
        image.data() + phdrs[interpPhdrIndex].p_offset);
  }

  if (sceProcParamIndex >= 0) {
    result->processParam =
        phdrs[sceProcParamIndex].p_vaddr
            ? reinterpret_cast<void *>(imageBase - baseAddress +
                                       phdrs[sceProcParamIndex].p_vaddr)
            : nullptr;
    result->processParamSize = phdrs[sceProcParamIndex].p_memsz;
  }

  if (sceModuleParamIndex >= 0) {
    result->moduleParam =
        phdrs[sceModuleParamIndex].p_vaddr
            ? reinterpret_cast<void *>(imageBase - baseAddress +
                                       phdrs[sceModuleParamIndex].p_vaddr)
            : nullptr;
    result->moduleParamSize = phdrs[sceModuleParamIndex].p_memsz;
  }

  if (tlsPhdrIndex >= 0) {
    result->tlsAlign = phdrs[tlsPhdrIndex].p_align;
    result->tlsSize = phdrs[tlsPhdrIndex].p_memsz;
    result->tlsInitSize = phdrs[tlsPhdrIndex].p_filesz;
    result->tlsInit =
        phdrs[tlsPhdrIndex].p_vaddr
            ? imageBase - baseAddress + phdrs[tlsPhdrIndex].p_vaddr
            : nullptr;
  }

  if (gnuEhFramePhdrIndex >= 0 && phdrs[gnuEhFramePhdrIndex].p_vaddr > 0) {
    result->ehFrameHdr =
        imageBase - baseAddress + phdrs[gnuEhFramePhdrIndex].p_vaddr;
    result->ehFrameHdrSize = phdrs[gnuEhFramePhdrIndex].p_memsz;

    struct GnuExceptionInfo {
      uint8_t version;
      uint8_t encoding;
      uint8_t fdeCount;
      uint8_t encodingTable;
      std::byte first;
    };

    auto *exinfo = reinterpret_cast<GnuExceptionInfo *>(
        image.data() + phdrs[gnuEhFramePhdrIndex].p_offset);

    if (exinfo->version != 1) {
      std::abort();
    }

    if (exinfo->fdeCount != 0x03) {
      std::abort();
    }

    if (exinfo->encodingTable != 0x3b) {
      std::abort();
    }

    std::byte *dataBuffer = nullptr;

    if (exinfo->encoding == 0x03) {
      auto offset = *reinterpret_cast<std::uint32_t *>(&exinfo->first);
      dataBuffer = imageBase - baseAddress + offset;
    } else if (exinfo->encoding == 0x1B) {
      auto offset = *reinterpret_cast<std::int32_t *>(&exinfo->first);
      dataBuffer = &exinfo->first + sizeof(std::int32_t) + offset;
    } else {
      std::abort();
    }

    auto *dataBufferIt = dataBuffer;
    while (true) {
      auto size = *reinterpret_cast<std::int32_t *>(dataBufferIt);
      dataBufferIt += sizeof(std::uint32_t);

      if (size == 0) {
        break;
      }

      if (size == -1) {
        size = *reinterpret_cast<std::uint64_t *>(dataBufferIt) +
               sizeof(std::uint64_t);
      }

      dataBufferIt += size;
    }

    result->ehFrame =
        imageBase - baseAddress + phdrs[gnuEhFramePhdrIndex].p_vaddr +
        (dataBuffer - image.data() - phdrs[gnuEhFramePhdrIndex].p_offset);
    result->ehFrameSize = dataBufferIt - dataBuffer;
  }

  orbis::Relocation *pltRelocations = nullptr;
  std::size_t pltRelocationCount = 0;
  orbis::Relocation *nonPltRelocations = nullptr;
  std::size_t nonPltRelocationCount = 0;

  if (dynamicPhdrIndex >= 0 && phdrs[dynamicPhdrIndex].p_filesz > 0) {
    auto &dynPhdr = phdrs[dynamicPhdrIndex];
    std::vector<Elf64_Dyn> dyns(dynPhdr.p_filesz / sizeof(Elf64_Dyn));
    std::memcpy(dyns.data(), image.data() + dynPhdr.p_offset,
                dyns.size() * sizeof(Elf64_Dyn));

    int sceStrtabIndex = -1;
    int sceSymtabIndex = -1;
    int strtabIndex = -1;
    int symtabIndex = -1;
    int hashIndex = -1;
    int sceSymtabSizeIndex = -1;
    for (auto &dyn : dyns) {
      if (dyn.d_tag == kElfDynamicTypeSceStrTab) {
        sceStrtabIndex = &dyn - dyns.data();
      } else if (dyn.d_tag == kElfDynamicTypeSceSymTab) {
        sceSymtabIndex = &dyn - dyns.data();
      } else if (dyn.d_tag == kElfDynamicTypeSceSymTabSize) {
        sceSymtabSizeIndex = &dyn - dyns.data();
      } else if (dyn.d_tag == kElfDynamicTypeStrTab) {
        strtabIndex = &dyn - dyns.data();
      } else if (dyn.d_tag == kElfDynamicTypeSymTab) {
        symtabIndex = &dyn - dyns.data();
      } else if (dyn.d_tag == kElfDynamicTypeHash) {
        hashIndex = &dyn - dyns.data();
      }
    }

    auto dynTabOffsetGet = [&](std::uint64_t address) {
      if (sceSymtabIndex < 0 || sceStrtabIndex < 0) {
        for (auto &phdr : phdrs) {
          if (phdr.p_vaddr <= address &&
              address < phdr.p_vaddr + phdr.p_memsz) {
            return phdr.p_offset + address - phdr.p_vaddr;
          }
        }
      }

      return address;
    };

    auto sceStrtab =
        sceStrtabIndex >= 0 && sceDynlibDataPhdrIndex >= 0
            ? reinterpret_cast<const char *>(
                  image.data() +
                  dynTabOffsetGet(dyns[sceStrtabIndex].d_un.d_val) +
                  phdrs[sceDynlibDataPhdrIndex].p_offset)
            : nullptr;

    auto strtab = sceStrtab;

    if (strtab == nullptr && strtabIndex >= 0) {
      strtab = reinterpret_cast<const char *>(
          image.data() + dynTabOffsetGet(dyns[strtabIndex].d_un.d_val));
    }

    auto sceDynlibData =
        sceDynlibDataPhdrIndex >= 0
            ? image.data() + phdrs[sceDynlibDataPhdrIndex].p_offset
            : nullptr;

    auto sceSymtabData =
        sceSymtabIndex >= 0 && sceDynlibData != nullptr
            ? reinterpret_cast<const Elf64_Sym *>(
                  sceDynlibData + dyns[sceSymtabIndex].d_un.d_val)
            : nullptr;

    auto symtab = sceSymtabData;
    auto symtabSize =
        sceSymtabSizeIndex >= 0
            ? dyns[sceSymtabSizeIndex].d_un.d_val / sizeof(Elf64_Sym)
            : 0;

    if (symtab == nullptr && symtabIndex >= 0) {
      if (hashIndex < 0) {
        std::fprintf(stderr, "SYMTAB without HASH!\n");
        std::abort();
      }

      symtab = reinterpret_cast<const Elf64_Sym *>(
          image.data() + dynTabOffsetGet(dyns[symtabIndex].d_un.d_val));
      symtabSize = *reinterpret_cast<const std::uint32_t *>(
          image.data() + dynTabOffsetGet(dyns[hashIndex].d_un.d_val) +
          sizeof(std::uint32_t));
    }

    std::unordered_map<std::uint64_t, std::size_t> idToModuleIndex;
    std::unordered_map<std::uint64_t, std::size_t> idToLibraryIndex;

    bool hasPs4Dyn = false;
    bool hasPs5Dyn = false;

    for (auto dyn : dyns) {
      if (dyn.d_tag == kElfDynamicTypeSceModuleInfo ||
          dyn.d_tag == kElfDynamicTypeSceModuleInfo1) {
        std::strncpy(result->moduleName,
                     strtab + static_cast<std::uint32_t>(dyn.d_un.d_val),
                     sizeof(result->moduleName));
      } else if (dyn.d_tag == kElfDynamicTypeSceModuleAttr) {
        result->attributes = dyn.d_un.d_val;
      }

      if (dyn.d_tag == kElfDynamicTypeSoName) {
        std::strncpy(result->soName,
                     strtab + static_cast<std::uint32_t>(dyn.d_un.d_val),
                     sizeof(result->soName));
      }

      if (dyn.d_tag == kElfDynamicTypeSceModuleInfo ||
          dyn.d_tag == kElfDynamicTypeSceModuleInfo1) {
        idToModuleIndex[dyn.d_un.d_val >> 48] = -1;
      }

      if (dyn.d_tag == kElfDynamicTypeSceNeededModule ||
          dyn.d_tag == kElfDynamicTypeSceNeededModule1) {
        if (dyn.d_tag == kElfDynamicTypeSceNeededModule) {
          hasPs4Dyn = true;
        } else {
          hasPs5Dyn = true;
        }
        auto [it, inserted] = idToModuleIndex.try_emplace(
            dyn.d_un.d_val >> 48, result->neededModules.size());

        if (inserted) {
          result->neededModules.emplace_back();
        }

        auto &mod = result->neededModules[it->second];
        mod.name = strtab + static_cast<std::uint32_t>(dyn.d_un.d_val);
        mod.attr = static_cast<std::uint16_t>(dyn.d_un.d_val >> 32);
        mod.isExport = false;
      } else if (dyn.d_tag == kElfDynamicTypeSceImportLib ||
                 dyn.d_tag == kElfDynamicTypeSceImportLib1 ||
                 dyn.d_tag == kElfDynamicTypeSceExportLib ||
                 dyn.d_tag == kElfDynamicTypeSceExportLib1) {
        auto [it, inserted] = idToLibraryIndex.try_emplace(
            dyn.d_un.d_val >> 48, result->neededLibraries.size());

        if (inserted) {
          result->neededLibraries.emplace_back();
        }

        auto &lib = result->neededLibraries[it->second];

        lib.name = strtab + static_cast<std::uint32_t>(dyn.d_un.d_val);
        lib.isExport = dyn.d_tag == kElfDynamicTypeSceExportLib ||
                       dyn.d_tag == kElfDynamicTypeSceExportLib1;

        if (dyn.d_tag == kElfDynamicTypeSceExportLib ||
            dyn.d_tag == kElfDynamicTypeSceImportLib) {
          hasPs4Dyn = true;
        } else {
          hasPs5Dyn = true;
        }
      } else if (dyn.d_tag == kElfDynamicTypeSceExportLibAttr ||
                 dyn.d_tag == kElfDynamicTypeSceImportLibAttr) {
        auto [it, inserted] = idToLibraryIndex.try_emplace(
            dyn.d_un.d_val >> 48, result->neededLibraries.size());

        if (inserted) {
          result->neededLibraries.emplace_back();
        }

        auto &lib = result->neededLibraries[it->second];

        lib.attr = dyn.d_un.d_val & ((static_cast<std::uint64_t>(1) << 48) - 1);
      }

      switch (dyn.d_tag) {
      case kElfDynamicTypePltGot:
      case kElfDynamicTypeScePltGot:
        result->pltGot = dyn.d_un.d_ptr
                             ? reinterpret_cast<std::uint64_t *>(
                                   imageBase - baseAddress + dyn.d_un.d_ptr)
                             : nullptr;
        break;

      case kElfDynamicTypeJmpRel:
        pltRelocations = reinterpret_cast<orbis::Relocation *>(
            imageBase - baseAddress + dyn.d_un.d_ptr);
        break;

      case kElfDynamicTypeSceJmpRel:
        if (sceDynlibData != nullptr) {
          pltRelocations = reinterpret_cast<orbis::Relocation *>(
              sceDynlibData + dyn.d_un.d_ptr);
        } else {
          pltRelocations = reinterpret_cast<orbis::Relocation *>(
              image.data() + dyn.d_un.d_ptr);
        }
        break;
      case kElfDynamicTypeScePltRel:
        break;
      case kElfDynamicTypePltRelSize:
      case kElfDynamicTypeScePltRelSize:
        pltRelocationCount = dyn.d_un.d_val / sizeof(orbis::Relocation);
        break;

      case kElfDynamicTypeRela:
        nonPltRelocations = reinterpret_cast<orbis::Relocation *>(
            imageBase - baseAddress + dyn.d_un.d_ptr);
        break;

      case kElfDynamicTypeSceRela:
        if (sceDynlibData != nullptr) {
          nonPltRelocations = reinterpret_cast<orbis::Relocation *>(
              sceDynlibData + dyn.d_un.d_ptr);
        } else {
          nonPltRelocations = reinterpret_cast<orbis::Relocation *>(
              image.data() + dyn.d_un.d_ptr);
        }
        break;
      case kElfDynamicTypeRelaSize:
      case kElfDynamicTypeSceRelaSize:
        nonPltRelocationCount = dyn.d_un.d_val / sizeof(orbis::Relocation);
        break;
      case kElfDynamicTypeSceRelaEnt:
        break;

      case kElfDynamicTypeInit:
        result->initProc = imageBase - baseAddress + dyn.d_un.d_ptr;
        break;
      case kElfDynamicTypeFini:
        result->finiProc = imageBase - baseAddress + dyn.d_un.d_ptr;
        break;
      }
    }

    if (hasPs4Dyn && hasPs5Dyn) {
      std::fprintf(stderr, "unexpected import type\n");
      std::abort();
    }

    if (!hasPs4Dyn && !hasPs5Dyn && interpPhdrIndex >= 0) {
      result->dynType = orbis::DynType::FreeBsd;
    } else if (hasPs4Dyn) {
      result->dynType = orbis::DynType::Ps4;
    } else if (hasPs5Dyn) {
      result->dynType = orbis::DynType::Ps5;
    }

    if (symtab != nullptr && symtabSize > 0) {
      auto sceSymtab = std::span(symtab, symtabSize);

      result->symbols.reserve(sceSymtab.size());

      for (auto &sym : sceSymtab) {
        auto visibility = ELF64_ST_VISIBILITY(sym.st_other);
        auto type = ELF64_ST_TYPE(sym.st_info);
        auto bind = ELF64_ST_BIND(sym.st_info);
        orbis::Symbol symbol{
            .address = sym.st_value,
            .size = sym.st_size,
            .visibility = static_cast<orbis::SymbolVisibility>(visibility),
            .bind = static_cast<orbis::SymbolBind>(bind),
            .type = static_cast<orbis::SymbolType>(type),
        };

        if (symbol.address) {
          symbol.address -= baseAddress;
        }

        if (strtab != nullptr && sym.st_name != 0) {
          auto fullName = std::string_view(strtab + sym.st_name);
          if (auto hashPos = fullName.find('#');
              hashPos != std::string_view::npos) {
            std::string_view module;
            std::string_view library;
            std::string_view name;

            name = fullName.substr(0, hashPos);
            auto moduleLibary = fullName.substr(hashPos + 1);

            hashPos = moduleLibary.find('#');

            if (hashPos == std::string_view::npos) {
              std::abort();
            }

            library = moduleLibary.substr(0, hashPos);
            module = moduleLibary.substr(hashPos + 1);

            auto libaryNid = *decodeNid(library);
            auto moduleNid = *decodeNid(module);

            symbol.libraryIndex = idToLibraryIndex.at(libaryNid);
            symbol.moduleIndex = idToModuleIndex.at(moduleNid);
            symbol.id = *decodeNid(name);
            if (name == "5JrIq4tzVIo") {
              monoPimpAddress = symbol.address + (std::uint64_t)imageBase;
              std::fprintf(stderr, "mono_pimp address = %lx\n",
                           monoPimpAddress);
            }
          } else if (auto nid = decodeNid(fullName)) {
            symbol.id = *nid;
            symbol.libraryIndex = -1;
            symbol.moduleIndex = -1;
          } else {
            symbol.id =
                encodeFid(strtab + static_cast<std::uint32_t>(sym.st_name));
            symbol.libraryIndex = -1;
            symbol.moduleIndex = -1;
          }
        }

        result->symbols.push_back(symbol);
      }
    }
  }

  for (auto phdr : phdrs) {
    if (phdr.p_type == kElfProgramTypeLoad ||
        phdr.p_type == kElfProgramTypeSceRelRo ||
        phdr.p_type == kElfProgramTypeGnuRelRo) {
      auto segmentEnd = rx::alignUp(phdr.p_vaddr + phdr.p_memsz, vm::kPageSize);
      auto segmentBegin =
          rx::alignDown(phdr.p_vaddr - baseAddress, phdr.p_align);
      auto segmentSize = segmentEnd - segmentBegin;
      ::mprotect(imageBase + segmentBegin, segmentSize, PROT_WRITE);
      std::memcpy(imageBase + phdr.p_vaddr - baseAddress,
                  image.data() + phdr.p_offset, phdr.p_filesz);

      if (phdr.p_type == kElfProgramTypeSceRelRo ||
          phdr.p_type == kElfProgramTypeGnuRelRo) {
        phdr.p_flags |= vm::kMapProtCpuWrite; // TODO: reprotect on relocations
      }

      int mapFlags = 0;

      if (phdr.p_flags & PF_X) {
        mapFlags |= vm::kMapProtCpuExec;
      }
      if (phdr.p_flags & PF_W) {
        mapFlags |= vm::kMapProtCpuWrite;
      }
      if (phdr.p_flags & PF_R) {
        mapFlags |= vm::kMapProtCpuRead;
      }

      if (mapFlags == 0) {
        mapFlags = vm::kMapProtCpuWrite;
      }

      vm::protect(imageBase + segmentBegin, segmentSize, mapFlags);

      if (phdr.p_type == kElfProgramTypeLoad) {
        if (result->segmentCount >= std::size(result->segments)) {
          std::abort();
        }

        auto &segment = result->segments[result->segmentCount++];
        segment.addr = imageBase + segmentBegin;
        segment.size = phdr.p_memsz;
        segment.prot = phdr.p_flags;
      }
    }
  }

  if (pltRelocations != nullptr && pltRelocationCount > 0) {
    result->pltRelocations.reserve(pltRelocationCount);
    for (auto rel : std::span(pltRelocations, pltRelocationCount)) {
      result->pltRelocations.push_back(rel);
    }
  }

  if (nonPltRelocations != nullptr && nonPltRelocationCount > 0) {
    result->nonPltRelocations.reserve(nonPltRelocationCount);
    for (auto rel : std::span(nonPltRelocations, nonPltRelocationCount)) {
      result->nonPltRelocations.push_back(rel);
    }
  }

  result->base = imageBase - baseAddress;
  result->size = imageSize + baseAddress;
  result->phdrAddress = phdrPhdrIndex >= 0 ? phdrs[phdrPhdrIndex].p_vaddr
                                           : baseAddress + header.e_phoff;
  result->phNum = header.e_phnum;
  result->proc = process;

  std::printf("Loaded module '%s' (%lx) from object '%s', address: %p - %p\n",
              result->moduleName, (unsigned long)result->attributes,
              result->soName, imageBase, (char *)imageBase + result->size);
  for (const auto &mod : result->neededModules) {
    std::printf("  needed module '%s' (%lx)\n", mod.name.c_str(),
                (unsigned long)mod.attr);
  }

  for (const auto &lib : result->neededLibraries) {
    std::printf("  needed library '%s' (%lx), kind %s\n", lib.name.c_str(),
                (unsigned long)lib.attr, lib.isExport ? "export" : "import");
  }

  if (tlsPhdrIndex >= 0 /* result->tlsSize != 0 */) {
    result->tlsIndex = process->nextTlsSlot++;
  }

  return result;
}

static Ref<orbis::Module> loadModuleFileImpl(std::string_view path,
                                             orbis::Thread *thread) {
  orbis::Ref<orbis::File> instance;
  if (vfs::open(path, kOpenFlagReadOnly, 0, &instance, thread).isError()) {
    return {};
  }

  orbis::Stat fileStat;
  if (instance->ops->stat(instance.get(), &fileStat, nullptr) !=
      orbis::ErrorCode{}) {
    return {};
  }

  auto len = fileStat.size;

  std::vector<std::byte> image(len);
  auto ptr = image.data();
  orbis::IoVec ioVec{
      .base = ptr,
      .len = static_cast<std::uint64_t>(len),
  };
  orbis::Uio io{
      .offset = 0,
      .iov = &ioVec,
      .iovcnt = 1,
      .resid = 0,
      .segflg = orbis::UioSeg::SysSpace,
      .rw = orbis::UioRw::Read,
      .td = thread,
  };

  while (io.offset < image.size()) {
    ioVec = {
        .base = ptr + io.offset,
        .len = image.size() - io.offset,
    };
    auto result = instance->ops->read(instance.get(), &io, thread);
    if (result != orbis::ErrorCode{}) {
      std::fprintf(stderr, "Module file reading error\n");
      std::abort();
    }
  }

  if (image[0] != std::byte{'\x7f'} || image[1] != std::byte{'E'} ||
      image[2] != std::byte{'L'} || image[3] != std::byte{'F'}) {
    image = unself(image.data(), image.size());

    // std::ofstream(
    //     std::filesystem::path(path).filename().replace_extension("elf"),
    //     std::ios::binary)
    //     .write((const char *)image.data(), image.size());
  }

  return rx::linker::loadModule(image, thread->tproc);
}

Ref<orbis::Module> rx::linker::loadModuleFile(std::string_view path,
                                              orbis::Thread *thread) {
  if (auto result = loadModuleFileImpl(path, thread)) {
    return result;
  }

  if (path.ends_with(".sprx")) {
    path.remove_suffix(4);

    if (auto result = loadModuleFileImpl(std::string(path) + "prx", thread)) {
      return result;
    }

    return nullptr;
  }

  if (path.ends_with(".prx")) {
    path.remove_suffix(3);

    if (auto result = loadModuleFileImpl(std::string(path) + "sprx", thread)) {
      return result;
    }

    return nullptr;
  }

  return nullptr;
}
