#include <hilbert/kernel/storage/bd/memory.hpp>
#include <hilbert/kernel/storage/fs/tarfs.hpp>
#include <hilbert/kernel/application.hpp>
#include <hilbert/kernel/framebuffer.hpp>
#include <hilbert/kernel/terminal.hpp>
#include <hilbert/kernel/limine.hpp>
#include <hilbert/kernel/paging.hpp>
#include <hilbert/kernel/vfile.hpp>

using namespace hilbert::kernel;

LIMINE_BASE_REVISION(1)

static volatile limine_memmap_request memmap_request {
  .id = LIMINE_MEMMAP_REQUEST,
  .revision = 0,
  .response = 0
};

static volatile limine_kernel_address_request kernel_address_request {
  .id = LIMINE_KERNEL_ADDRESS_REQUEST,
  .revision = 0,
  .response = 0
};

static volatile limine_framebuffer_request framebuffer_request {
  .id = LIMINE_FRAMEBUFFER_REQUEST,
  .revision = 0,
  .response = 0
};

static volatile limine_hhdm_request hhdm_request {
  .id = LIMINE_HHDM_REQUEST,
  .revision = 0,
  .response = 0
};

static limine_internal_module initfs_module = {
  .path = "initfs.tgz",
  .cmdline = "initfs",
  .flags = LIMINE_INTERNAL_MODULE_REQUIRED | LIMINE_INTERNAL_MODULE_COMPRESSED
};

static limine_internal_module termfont_module = {
  .path = "termfont.psf",
  .cmdline = "termfont",
  .flags = LIMINE_INTERNAL_MODULE_REQUIRED
};

static limine_internal_module *internal_modules[] = {
  &initfs_module, &termfont_module
};

static volatile limine_module_request module_request = {
  .id = LIMINE_MODULE_REQUEST,
  .revision = 2,
  .response = 0,
  .internal_module_count = 2,
  .internal_modules = internal_modules
};

bool try_map_module_by_cmdline(
  const char *cmdline, void *&vaddr_out, uint64_t &len_out
) {
  auto response = module_request.response;
  for (uint64_t i = 0; i < response->module_count; ++i) {
    limine_file *file = response->modules[i];
    for (uint64_t j = 0; cmdline[j] == file->cmdline[j]; ++j)
      if (!cmdline[j]) {

        //module start is guaranteed to be page-aligned, end is not.
        uint64_t start_paddr =
          (uint64_t)file->address - hhdm_request.response->offset;
        uint64_t end_paddr =
          ((start_paddr + file->size - 1) / 4096 + 1) * 4096;

        uint64_t start_vaddr =
          paging::find_unmapped_vram_region((end_paddr - start_paddr) / 4096);
        for (uint64_t i = 0; i < end_paddr - start_paddr; i += 4096)
          paging::map_kernel_page(
            start_paddr + i, start_vaddr + i, true, false);

        vaddr_out = (void *)start_vaddr;
        len_out = file->size;

        return true;
      }
  }
  return false;
}

//defined in linker script, page-aligned:
extern uint8_t __kernel_start;
extern uint8_t __kernel_end;
extern uint8_t __kernel_rx_start;
extern uint8_t __kernel_rx_end;
extern uint8_t __kernel_ro_start;
extern uint8_t __kernel_ro_end;
extern uint8_t __kernel_rw_start;
extern uint8_t __kernel_rw_end;

uint8_t *initfs;
uint64_t initfs_len;

[[noreturn]] static void with_kernel_p4();

extern "C" void load_gdt_and_idt();

extern "C" [[noreturn]] void entry() {

  //TODO?: maybe we should check if the limine requests were
  //       fulfilled and display some error message if not

  //set up the physical memory usage bitmap:

  paging::mark_all_pram_used();
  auto memmap = memmap_request.response;
  for (uint64_t i = 0; i < memmap->entry_count; ++i) {
    //we don't allocate any physical pages until after we are done using limine
    //structures (specifically at the call to paging::map_kernel_stacks), so
    //we consider bootloader reclaimable to be free. usable and bootloader
    //reclaimable are guaranteed by limine spec to be page-aligned.
    auto entry = memmap->entries[i];
    if (entry->type == LIMINE_MEMMAP_USABLE ||
        entry->type == LIMINE_MEMMAP_BOOTLOADER_RECLAIMABLE)
      paging::mark_pram_region_free(entry->base, entry->base + entry->length);
  }

  //set up page mappings:

  auto kernel_address = kernel_address_request.response;
  uint64_t kernel_offset = kernel_address->virtual_base
                         - kernel_address->physical_base;

  uint64_t hhdm = hhdm_request.response->offset;

  //framebuffer might not be page-aligned
  auto framebuffer = framebuffer_request.response->framebuffers[0];
  uint64_t fb_start = ((uint64_t)framebuffer->address / 4096) * 4096 - hhdm;
  uint64_t fb_end = (uint64_t)framebuffer->address
                  + framebuffer->pitch * framebuffer->height - hhdm;
  fb_end = ((fb_end - 1) / 4096 + 1) * 4096;

  paging::init_kernel_page_tables(kernel_offset);

  //kernel image rx
  for (uint64_t vaddr = (uint64_t)&__kernel_rx_start;
       vaddr < (uint64_t)&__kernel_rx_end; vaddr += 4096)
    paging::map_kernel_page(vaddr - kernel_offset, vaddr, false, true);

  //kernel image ro
  for (uint64_t vaddr = (uint64_t)&__kernel_ro_start;
       vaddr < (uint64_t)&__kernel_ro_end; vaddr += 4096)
    paging::map_kernel_page(vaddr - kernel_offset, vaddr, false, false);

  //kernel image rw
  for (uint64_t vaddr = (uint64_t)&__kernel_rw_start;
       vaddr < (uint64_t)&__kernel_rw_end; vaddr += 4096)
    paging::map_kernel_page(vaddr - kernel_offset, vaddr, true, false);

  //framebuffer
  uint64_t fb_vaddr =
    paging::find_unmapped_vram_region((fb_end - fb_start) / 4096);
  for (uint64_t i = 0; i < fb_end - fb_start; i += 4096)
    paging::map_kernel_page(fb_start + i, fb_vaddr + i, true, false);

  //initfs and termfont - these are required modules
  //so there is no worry about them not being present.
  try_map_module_by_cmdline("initfs", (void *&)initfs, initfs_len);
  try_map_module_by_cmdline(
    "termfont", (void *&)terminal::termfont, terminal::termfont_len);

  //set up framebuffer and terminal:
  //TODO: assumes framebuffer is 32-bpp rgb

  framebuffer::init_framebuffer(fb_start, fb_vaddr,
    framebuffer->width, framebuffer->height, framebuffer->pitch);

  //switch to kernel p4

  paging::map_kernel_stacks();
  load_gdt_and_idt();
  switch_to_kernel_p4(&with_kernel_p4);

}

[[noreturn]] static void print_and_halt(const char *msg) {
  terminal::put_string_sz(msg);
  while (1)
    asm ("hlt");
}

extern "C" [[noreturn]] void start_user_mode(
  uint64_t rip, uint64_t rsp, uint64_t p4_paddr);

[[noreturn]] static void with_kernel_p4() {

  terminal::init_terminal();

  storage::bd::memory initfs_bd(initfs, initfs_len);
  storage::fs::tarfs_instance initfs_fs(&initfs_bd);
  initfs_bd.mounted_as = &initfs_fs;

  vfile::vfile vfs_root;
  vfs_root.bd = &initfs_bd;
  vfs_root.dir_entry.type = storage::file_type::directory;
  vfs_root.path.absolute = true;

  if (initfs_fs.get_root_node(vfs_root.dir_entry.node) !=
      storage::fs_result::success)
    print_and_halt("failed to get root node of initfs.");

  utility::string init_path_string("/bin/init.elf", 13);
  vfile::canon_path init_path;
  vfile::canonize_path(init_path_string, init_path);

  std::optional<vfile::vfile> init_file;
  if (vfile::lookup_path(vfs_root, init_path, init_file) !=
      storage::fs_result::success)
    print_and_halt("failed to look up /bin/init.elf.");
  if (!init_file)
    print_and_halt("/bin/init.elf does not exist.");

  application::app_instance *init;
  if (application::create_app(*init_file, init) !=
      application::create_app_result::success)
    print_and_halt("failed to parse /bin/init.elf.");

  application::running_app = init;
  start_user_mode(init->saved_regs.rip, init->saved_regs.rsp, init->p4_paddr);

}