#include <Kyoto/Alloc/CGameAllocator.hpp>

#include <Kyoto/Alloc/CCallStack.hpp>
#include <Kyoto/Alloc/CMediumAllocPool.hpp>
#include <Kyoto/Alloc/CSmallAllocPool.hpp>
#include <Kyoto/Basics/COsContext.hpp>
#include <Kyoto/Basics/CStopwatch.hpp>

#include <stdint.h>

/* Here just to make sure the data section matches */
static const char* string_NULL = "<NULL>";
static const char* string_SOURCE_MODULE_UNLOADED = "<SOURCE MODULE UNLOADED>";
static const char* string_ = "";
static int gAllocatorTime = 0;

template < typename U1, typename U2 >
static inline U1 T_round_up(U2 val, int align) {
  return (val + (align - 1)) & ~(align - 1);
}

CGameAllocator::SGameMemInfo* CGameAllocator::GetMemInfoFromBlockPtr(const void* ptr) const {
  return (SGameMemInfo*)((uchar*)(ptr) - sizeof(SGameMemInfo));
}

CGameAllocator::CGameAllocator()
: x4_(0)
, x8_heapSize(0)
, xc_first(nullptr)
, x10_last(nullptr)
, x54_(0)
, x58_oomCallback(nullptr)
, x5c_oomTarget(nullptr)
, x60_smallAllocPool(nullptr)
, x64_smallAllocMainData(nullptr)
, x68_smallAllocBookKeeping(nullptr)
, x6c_(false)
, x70_(0)
, x74_mediumPool(nullptr)
, x80_(0)
, x84_(0)
, x88_(0)
, x8c_(0)
, x90_heapSize2(0)
, x94_(0)
, x98_(0)
, x9c_(0)
, xa0_(0)
, xa4_(0)
, xa8_(0)
, xac_(0)
, xb0_(0)
, xb4_physicalAddr(nullptr)
, xb8_fakeStatics(0)
, xbc_(0) {}

CGameAllocator::~CGameAllocator() {
  if (x74_mediumPool) {
    x74_mediumPool->ClearPuddles();
    FreeNormalAllocation(x74_mediumPool);
    x74_mediumPool = nullptr;
  }
}

bool CGameAllocator::Initialize(COsContext& ctx) { return false; }

void CGameAllocator::Shutdown() {
  ReleaseAll();
  x4_ = 0;
  x54_ = 0;
}

void* CGameAllocator::Alloc(size_t size, EHint hint, EScope scope, EType type,
                            const CCallStack& callstack) {
  OSTick startTick = OSGetTick();

  if (hint & kHI_RoundUpLen) {
    size = T_round_up< size_t, size_t >(size, 32);
  }

  bool bVar1 = size <= 56 && !(hint & (kHI_RoundUpLen | kHI_TopOfHeap)) && x60_smallAllocPool;

  if (bVar1 && x70_ > 0) {
    bVar1 = false;
    --x70_;
  }

  if (bVar1) {
    void* buf = x60_smallAllocPool->Alloc(size);
    uint tmp = x60_smallAllocPool->GetAllocatedSize();
    if (xac_ < tmp) {
      xac_ = tmp;
      static int sLastSmallAllocSize = 0;
      if (sLastSmallAllocSize + 128 < tmp) {
        sLastSmallAllocSize = tmp;
      }
    }

    if (buf != nullptr) {
      gAllocatorTime += (OSGetTick() - startTick);
      return buf;
    }
    x70_ = 25;
    x6c_ = true;
  }

  if (x74_mediumPool && size <= 0x400 && !(hint & kHI_TopOfHeap)) {
    void* buf = nullptr;
    if (!x74_mediumPool->HasPuddles()) {
      x74_mediumPool->AddPuddle(0x1000, x78_, 0);
      x78_ = nullptr;
    }

    buf = x74_mediumPool->Alloc(size);

    if (buf == nullptr) {
      buf = Alloc(0x21000, kHI_None, kSC_Unk1, kTP_Heap,
                  CCallStack(-1, "MediumAllocMainData   ", " - Ignore"));
      x74_mediumPool->AddPuddle(0x1000, buf, 1);
      buf = x74_mediumPool->Alloc(size);
    }

    if (buf != nullptr) {
      gAllocatorTime += OSGetTick() - startTick;
      return buf;
    }

    if (!x7c_) {
      x74_mediumPool->GetTotalEntries();
      x74_mediumPool->GetNumAllocs();
      x74_mediumPool->GetNumBlocksAvailable();
    }
    x7c_ = true;
  }

  size_t roundedSize = T_round_up< size_t, size_t >(size, 32);
  SGameMemInfo* info = nullptr;

  if (hint & kHI_TopOfHeap) {
    info = FindFreeBlockFromTopOfHeap(roundedSize);
  } else {
    info = FindFreeBlock(roundedSize);
  }

  if (info == nullptr) {
    void* mediumBuf = nullptr;
    if (x58_oomCallback) {
      x58_oomCallback(x5c_oomTarget, size);

      static bool bTriedCallback = false;
      if (!bTriedCallback) {
        bTriedCallback = true;
        mediumBuf = Alloc(size, hint, scope, type, callstack);
        bTriedCallback = false;
      } else {
        return nullptr;
      }
    }
    if (mediumBuf == nullptr) {
      callstack.GetFileAndLineText();
      callstack.GetTypeText();
      DumpAllocations();
      return nullptr;
    }
    return mediumBuf;
  }

  uint tmp = FixupAllocPtrs(info, size, roundedSize, hint, callstack);
  if ((hint & kHI_TopOfHeap) != 0u && !info->IsAllocated()) {
    info = info->GetNext();
  }

  UpdateAllocDebugStats(size, roundedSize, tmp);
  gAllocatorTime += OSGetTick() - startTick;
  return ++info;
}

CGameAllocator::SGameMemInfo* CGameAllocator::FindFreeBlock(uint) { return nullptr; }

CGameAllocator::SGameMemInfo* CGameAllocator::FindFreeBlockFromTopOfHeap(uint size) {
  SGameMemInfo* iter = x10_last;
  SGameMemInfo* ret = nullptr;

  while (iter != nullptr) {
    if (!iter->IsAllocated() && iter->GetLength() >= size) {
      ret = iter;
      break;
    }
    iter = iter->GetPrev();
  }

  RemoveFreeEntryFromFreeList(ret);
  return ret;
}

uint CGameAllocator::FixupAllocPtrs(SGameMemInfo*, uint, uint, EHint, const CCallStack&) {
  return 0;
}

void CGameAllocator::UpdateAllocDebugStats(uint len, uint roundedLen, uint offset) {
  ++x84_;
  ++x80_;
  x88_ += len;
  x8c_ += roundedLen + offset;
  x90_heapSize2 -= roundedLen + offset;

  if (x84_ > x94_) {
    x94_ = x84_;
  }

  if (x8c_ > x98_) {
    x98_ = x8c_;
  }

  if (len < x9c_) {
    x9c_ = len;
  }

  if (len > xa0_) {
    xa0_ = len;
  }
  xa4_ = (len + xa4_ * (x80_ - 1)) / x80_;
  if (len > 56) {
    return;
  }

  ++xa8_;
}

bool CGameAllocator::Free(const void* ptr) {
  if (ptr == nullptr) {
    return true;
  }

  if (x60_smallAllocPool && x60_smallAllocPool->PtrWithinPool(ptr)) {
    return x60_smallAllocPool->Free(ptr);
  }

  if (x74_mediumPool) {
    int tmp = x74_mediumPool->Free(ptr);
    if (tmp != 1) {
      return tmp > 0;
    }
  }
  return FreeNormalAllocation(ptr);
}

bool CGameAllocator::FreeNormalAllocation(const void*) { return true; };

void CGameAllocator::ReleaseAll() {
  if (x74_mediumPool) {
    x74_mediumPool->ClearPuddles();
    FreeNormalAllocation(x74_mediumPool);
    x74_mediumPool = nullptr;
  }

  SGameMemInfo* iter = xc_first;
  while (iter != nullptr) {
    SGameMemInfo* next = iter->GetNext();
    if (iter->IsAllocated()) {
      FreeNormalAllocation(((uchar*)iter) + sizeof(SGameMemInfo));
    }
    iter = next;
  }

  xc_first = nullptr;
  x10_last = nullptr;
};

void* CGameAllocator::AllocSecondary(size_t size, EHint hint, EScope scope, EType type,
                                     const CCallStack& callstack) {
  return Alloc(size, hint, scope, type, callstack);
};

bool CGameAllocator::FreeSecondary(const void* ptr) { return Free(ptr); };

void CGameAllocator::ReleaseAllSecondary(){};

void CGameAllocator::SetOutOfMemoryCallback(FOutOfMemoryCb cb, const void* target) {
  x58_oomCallback = cb;
  x5c_oomTarget = target;
};

IAllocator::SAllocInfo CGameAllocator::GetAllocInfo(const void* ptr) const {
  SGameMemInfo* info = GetMemInfoFromBlockPtr(ptr);

  return SAllocInfo((const void*)info, info->GetLength(), info->IsAllocated(), false,
                    info->x8_fileAndLine, info->xc_type);
};

IAllocator::SMetrics CGameAllocator::GetMetrics() const {
  uint mediumAllocTotalAllocated =
      x74_mediumPool != nullptr ? x74_mediumPool->GetTotalEntries() * 32 : 0;
  uint mediumAllocBlocksAvailable =
      x74_mediumPool != nullptr ? x74_mediumPool->GetNumBlocksAvailable() : 0;
  uint mediumAllocAllocatedSize =
      x74_mediumPool != nullptr
          ? x74_mediumPool->GetTotalEntries() - x74_mediumPool->GetNumBlocksAvailable()
          : 0;
  uint mediumAllocNumAllocs = x74_mediumPool != nullptr ? x74_mediumPool->GetNumAllocs() : 0;
  SMetrics ret(x8_heapSize, x80_, x84_, x88_, x8c_, x90_heapSize2, x94_, x98_, x9c_, xa0_, xa4_,
               x60_smallAllocPool != nullptr ? x60_smallAllocPool->GetNumAllocs() : 0,
               x60_smallAllocPool != nullptr ? x60_smallAllocPool->GetAllocatedSize() : 0,
               x60_smallAllocPool != nullptr ? x60_smallAllocPool->GetNumBlocksAvailable() : 0,
               mediumAllocNumAllocs, mediumAllocAllocatedSize, mediumAllocBlocksAvailable,
               x80_ - xb0_, (uintptr_t)xb4_physicalAddr, xbc_, mediumAllocTotalAllocated,
               xb8_fakeStatics);
  xb0_ = x80_;
  return ret;
};

int CGameAllocator::EnumAllocations(FEnumAllocationsCb func, const void* ptr, bool b) const {

  int i = 0;
  SGameMemInfo* iter = xc_first;

  while (iter != nullptr) {
    if (!iter->IsPostGuardIntact()) {
      return -1;
    }

    if (!iter->IsPriorGuardIntact()) {
      return -1;
    }

    SGameMemInfo* next = iter->GetNext();
    SAllocInfo alloc((const void*)iter, iter->GetLength(), iter->IsAllocated(), false,
                     iter->x8_fileAndLine, iter->xc_type);
    func(alloc, ptr);
    ++i;
    iter = next;
  }

  return i;
};

uint CGameAllocator::GetFreeBinEntryForSize(uint size) {
  uint maxLen = 0x20;
  uint bin = 0;

  while (maxLen < 0x200000) {
    if (size < maxLen) {
      return bin;
    }

    maxLen <<= 1;
    ++bin;
  }

  return 0xf;
}

void CGameAllocator::AddFreeEntryToFreeList(SGameMemInfo* info) {
  uint bin = GetFreeBinEntryForSize(info->GetLength());
  info->SetNextFree(x14_bins[bin]);
  x14_bins[bin] = info;
}

void CGameAllocator::RemoveFreeEntryFromFreeList(SGameMemInfo* memInfo) {
  uint bin = GetFreeBinEntryForSize(memInfo->GetLength());
  SGameMemInfo* curBin = nullptr;
  SGameMemInfo* binIt = x14_bins[bin];

  while (binIt != nullptr) {
    if (binIt == memInfo) {
      if (curBin == nullptr) {
        x14_bins[bin] = binIt->GetNextFree();
      } else {
        curBin->SetNextFree(binIt->GetNextFree());
      }
      return;
    }

    curBin = binIt;
    binIt = binIt->GetNextFree();
  }
}

static inline bool DoWait(int v) { return (v % 4) == 0; }

void CGameAllocator::DumpAllocations() const {
  GetLargestFreeChunk();
  uint i = 0;
  SGameMemInfo* iter = xc_first;

  while (iter != nullptr) {
    ++i;

    if (DoWait(i)) {
      CStopwatch::Wait(0.005f);
    }
    iter = iter->GetNext();
  }
}

size_t CGameAllocator::GetLargestFreeChunk() const {
  SGameMemInfo* iter = xc_first;
  size_t ret = 0;
  while (iter != nullptr) {
    if (!iter->IsAllocated() && iter->GetLength() > ret) {
      ret = iter->GetLength();
    }
    iter = iter->GetNextFree();
  }

  return ret;
}
void CGameAllocator::OffsetFakeStatics(int offset) { xb8_fakeStatics += offset; }
