#include <hilbert/kernel/paging.hpp>
#include <stddef.h>

namespace hilbert::kernel::allocator {

  struct free_entry {
    uint64_t start;
    uint64_t len;//0 for unused
  };

  struct free_page {
    free_page *next;
    free_entry entries[255];
  };

  free_page *first_page;

  static_assert(sizeof(free_page) == 4088);

  free_entry *get_entry(uint64_t start, uint64_t len) {
    for (free_page *fp = first_page; fp; fp = fp->next)
      for (int i = 0; i < 255; ++i)
        if (fp->entries[i].start == start && fp->entries[i].len == len)
          return fp->entries + i;
    return 0;
  }

  void add_entry(uint64_t start, uint64_t len) {
    for (free_page *fp = first_page; fp; fp = fp->next)
      for (int i = 0; i < 255; ++i)
        if (fp->entries[i].len == 0) {
          fp->entries[i].start = start;
          fp->entries[i].len = len;
          return;
        }
    free_page *new_page = (free_page *)paging::map_new_kernel_pages(1);
    new_page->next = first_page;
    first_page = new_page;
    for (int i = 2; i < 255; ++i)
      new_page->entries[i].len = 0;
    new_page->entries[0].start = (uint64_t)new_page + 4088;
    new_page->entries[0].len = 8;
    new_page->entries[1].start = start;
    new_page->entries[1].len = len;
  }

  //len is power of 2, start is len-aligned
  void free_block(uint64_t start, uint64_t len) {
    free_entry *buddy = get_entry(start ^ len, len);
    if (buddy) {
      buddy->start = start & ~len;
      buddy->len = len * 2;
    }
    else
      add_entry(start, len);
  }

  void free_region(uint64_t start, uint64_t len) {
    uint64_t block_size = 1;
    while (block_size <= len) {
      if (start & block_size) {
        free_block(start, block_size);
        start += block_size;
        len -= block_size;
      }
      block_size *= 2;
    }
    while (len) {
      block_size /= 2;
      if (block_size <= len) {
        free_block(start, block_size);
        start += block_size;
        len -= block_size;
      }
    }
    //testing
    if (len != 0)
      while (1)
        ;
  }

  uint64_t take_region(uint64_t len) {

    uint64_t min_size = 1;
    while (min_size < len)
      min_size *= 2;

    free_entry *entry = 0;

    for (free_page *fp = first_page; fp; fp = fp->next)
      for (int i = 0; i < 255; ++i)
        if (fp->entries[i].len >= min_size) {
          if (fp->entries[i].len == min_size) {
            entry = fp->entries + i;
            goto loop_done;
          }
          if (entry == 0 || fp->entries[i].len < entry->len)
            entry = fp->entries + i;
        }

  loop_done:
    if (entry != 0) {
      uint64_t start = entry->start;
      uint64_t block_len = entry->len;
      entry->len = 0;
      if (block_len != len)
        free_region(start + len, block_len - len);
      return start;
    }

    uint64_t pages = (len - 1) / 4096 + 1;
    uint64_t start = (uint64_t)paging::map_new_kernel_pages(pages);
    if (pages * 4096 != len)
      free_region(start + len, pages * 4096 - len);
    return start;

  }

}

using namespace hilbert::kernel::allocator;

void *_new(size_t len) {
  if (len == 0)
    return 0;
  uint64_t vaddr = take_region(len + sizeof(size_t));
  *(size_t *)vaddr = len;
  return (void *)(vaddr + sizeof(size_t));
}

void _delete(void *ptr) {
  if ((uint64_t)ptr == 0)
    return;
  uint64_t vaddr = (uint64_t)ptr - sizeof(size_t);
  free_region(vaddr, *(size_t *)vaddr + sizeof(size_t));
}

void *operator new(size_t len) {
  return _new(len);
}

void operator delete(void *ptr, size_t) {
  return _delete(ptr);
}

void operator delete(void *ptr) {
  return _delete(ptr);
}

void *operator new[](size_t len) {
  return _new(len);
}

void operator delete[](void *ptr, size_t) {
  return _delete(ptr);
}

void operator delete[](void *ptr) {
  return _delete(ptr);
}
