#include <hilbert/kernel/load-app.hpp>
#include <hilbert/kernel/paging.hpp>

namespace hilbert::kernel {

  struct elf_header {
    uint8_t fixed[24];
    uint64_t entry_point;
    uint64_t program_header_offset;
    uint64_t section_header_offset;
    uint32_t flags;
    uint16_t elf_header_length;
    uint16_t program_header_pitch;
    uint16_t program_header_count;
  };

  struct program_header {
    uint32_t type;
    uint32_t flags;
    uint64_t foffset;
    uint64_t vaddr;
    uint64_t paddr;
    uint64_t flength;
    uint64_t vlength;
  };

  struct load_info {
    uint64_t vaddr;
    uint64_t vpages_start;
    uint64_t vpages_count;
    uint64_t foffset;
    uint64_t flength;
    bool writable;
    bool executable;
  };

  static uint8_t expected_fixed_header[24] = {
    0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00
  };

  load_app_result load_app(
    vfile::vfile &file, app_memory &into, uint64_t &entry_out) {

    if (file.dir_entry.type != storage::file_type::regular_file)
      return load_app_result::not_app;

    if (file.dir_entry.length < sizeof(elf_header))
      return load_app_result::not_app;

    elf_header eh;
    if (file.read_file(0, sizeof(elf_header), &eh)
        != storage::fs_result::success)
      return load_app_result::io_error;

    for (int i = 0; i < 24; ++i)
      if (eh.fixed[i] != expected_fixed_header[i])
        return load_app_result::not_app;

    if (eh.entry_point < 0x1000 || eh.entry_point >= 0x4000000000)
      return load_app_result::not_app;

    utility::vector<load_info> load_infos;

    for (int i = 0; i < eh.program_header_count; ++i) {

      uint64_t offset = eh.program_header_offset + eh.program_header_pitch * i;
      if (offset + sizeof(program_header) > file.dir_entry.length)
        return load_app_result::not_app;

      program_header ph;
      if (file.read_file(offset, sizeof(program_header), &ph)
          != storage::fs_result::success)
        return load_app_result::io_error;

      if (ph.type == 1) {

        uint64_t vpages_start = (ph.vaddr / 4096) * 4096;
        uint64_t vpages_end = ((ph.vaddr + ph.vlength - 1) / 4096 + 1) * 4096;

        if (vpages_start < 0x1000 || vpages_end >= 0x4000000000 ||
            ph.foffset + ph.flength > file.dir_entry.length)
          return load_app_result::not_app;

        load_infos.add_end((load_info){
          .vaddr = ph.vaddr,
          .vpages_start = vpages_start,
          .vpages_count = (vpages_end - vpages_start) / 4096,
          .foffset = ph.foffset,
          .flength = ph.flength,
          .writable = (ph.flags & 2) != 0,
          .executable = (ph.flags & 4) != 0 });

      }

    }

    for (unsigned i = 0; i < load_infos.count; ++i) {
      const auto &li = load_infos.buffer[i];

      for (uint64_t pi = 0; pi < li.vpages_count; ++pi) {

        uint64_t page_user_vaddr = li.vpages_start + pi * 4096;

        uint64_t page_kernel_vaddr;
        uint64_t page_paddr;
        paging::map_new_kernel_page(page_kernel_vaddr, page_paddr);

        uint8_t *ptr = (uint8_t *)page_kernel_vaddr;
        int bytes_left = 4096;
        int64_t foffset = page_user_vaddr - li.vaddr;

        if (foffset < 0) {
          int to_skip = -foffset;
          for (int i = 0; i < to_skip; ++i)
            ptr[i] = 0;
          ptr += to_skip;
          bytes_left -= to_skip;
          foffset = 0;
        }

        int64_t left_in_file = li.flength - foffset;
        if (left_in_file > 0) {
          int to_read = left_in_file < bytes_left ? left_in_file : bytes_left;
          if (file.read_file(li.foffset + foffset, to_read, ptr) !=
              storage::fs_result::success) {
            paging::unmap_kernel_page((uint64_t)page_kernel_vaddr);
            paging::free_pram_page(page_paddr);
            return load_app_result::io_error;
          }
          ptr += to_read;
          bytes_left -= to_read;
        }

        if (bytes_left > 0)
          for (int i = 0; i < bytes_left; ++i)
            ptr[i] = 0;

        paging::unmap_kernel_page((uint64_t)page_kernel_vaddr);
        into.map_page(
          page_user_vaddr, page_paddr, li.writable, li.executable, true);

      }

    }

    entry_out = eh.entry_point;
    return load_app_result::success;

  }

}