#pragma once

#include "../../kernel/memory/memory.h"
#include <stdint.h>

struct gdt_row_t {
  uint16_t limit_low;
  uint16_t base_low;
  uint8_t  base_middle;
  uint8_t  access;
  uint8_t  granularity_limit_high;
  uint8_t  base_high;
} __attribute__((packed));

struct gdt_ptr_t {
  uint16_t size;
  uint32_t address;
} __attribute__((packed));

struct tss_row_t {
  uint32_t prev_tss;
  uint32_t esp_ring0;
  uint32_t ss_ring0;
  uint32_t padding[23]; // Extra options RAZZLE doesn't use
} __attribute__((packed));

enum gdt_access_t {
  GDT_ACCESS_PRESENT = 0b10000000,
  GDT_ACCESS_DPL_KERNEL = 0b00000000,
  GDT_ACCESS_DPL_USER = 0b01100000,
  GDT_ACCESS_CODEDATA = 0b00010000,
  GDT_ACCESS_EXECUTABLE = 0b00001000,
  GDT_ACCESS_GROW_DOWN = 0b00000100,
  GDT_ACCESS_RW = 0b00000010,
  GDT_ACCESS_ACCESSED = 0b00000001,
};

enum gdt_granularity_t {
  GDT_GRANULARITY_4K = 0b10001111,
  GDT_GRANULARITY_MODE_32BIT = 0b01001111,
};

struct tss_row_t tss_entry;

#define GDT_SIZE (6)
struct gdt_row_t gdt[GDT_SIZE];

void load_gdt(struct gdt_ptr_t *gp) {
  asm volatile("lgdt (%0)\n\t"
               "mov $0x10, %%ax\n\t"
               "mov %%ax, %%ds\n\t"
               "mov %%ax, %%es\n\t"
               "mov %%ax, %%fs\n\t"
               "mov %%ax, %%gs\n\t"
               "mov %%ax, %%ss\n\t"
               :
               : "r"(gp)
               : "%ax");
}

void set_gdt_row(uint32_t num, uint32_t base, uint32_t limit, uint8_t access,
                 uint8_t granularity) {
  gdt[num].base_low = base & 0xFFFF;
  gdt[num].base_middle = (base >> 16) & 0xFF;
  gdt[num].base_high = (base >> 24) & 0xFF;
  gdt[num].limit_low = limit & 0xFFFF;
  gdt[num].granularity_limit_high = (limit >> 16) & 0x0F;
  gdt[num].granularity_limit_high |= granularity & 0xF0;
  gdt[num].access = access;
}

void load_tss(uint16_t gdt_row_num) {
  gdt_row_num *= 8;
  __asm__ volatile("mov %0, %%ax; ltr %%ax" : : "r"(gdt_row_num) : "ax");
}

#define KERNEL_CODE_SEL (0x08)
#define KERNEL_DATA_SEL (0x10)

void init_gdt() {
  struct gdt_ptr_t gp;
  gp.size = (sizeof(struct gdt_row_t) * GDT_SIZE) - 1;
  gp.address = (uint32_t)&gdt;

  set_gdt_row(0, 0, 0, 0, 0);

  // Kernelmode code/data degments
  set_gdt_row(1, 0, 0xFFFFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_DPL_KERNEL | GDT_ACCESS_CODEDATA |
                  GDT_ACCESS_EXECUTABLE | GDT_ACCESS_RW | GDT_ACCESS_ACCESSED,
              GDT_GRANULARITY_4K | GDT_GRANULARITY_MODE_32BIT);
  set_gdt_row(2, 0, 0xFFFFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_DPL_KERNEL | GDT_ACCESS_CODEDATA |
                  GDT_ACCESS_RW | GDT_ACCESS_ACCESSED,
              GDT_GRANULARITY_4K | GDT_GRANULARITY_MODE_32BIT);

  // Usermode code/data segments
  set_gdt_row(3, 0x0000000, 0xFFFFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_DPL_USER | GDT_ACCESS_CODEDATA |
                  GDT_ACCESS_EXECUTABLE | GDT_ACCESS_RW | GDT_ACCESS_ACCESSED,
              GDT_GRANULARITY_4K | GDT_GRANULARITY_MODE_32BIT);
  set_gdt_row(4, 0x0000000, 0xFFFFFFFF,
              GDT_ACCESS_PRESENT | GDT_ACCESS_DPL_USER | GDT_ACCESS_CODEDATA |
                  GDT_ACCESS_RW | GDT_ACCESS_ACCESSED,
              GDT_GRANULARITY_4K | GDT_GRANULARITY_MODE_32BIT);

  // TSS row
  set_gdt_row(5, (uint32_t)&tss_entry, sizeof(tss_entry),
              GDT_ACCESS_PRESENT | GDT_ACCESS_EXECUTABLE | GDT_ACCESS_ACCESSED,
              GDT_GRANULARITY_4K | GDT_GRANULARITY_MODE_32BIT);

  // Setup TSS
  int kstack = 0;
  memset((char *)&tss_entry, 0, sizeof(tss_entry));
  tss_entry.ss_ring0 = 0x10;
  tss_entry.esp_ring0 = (uint32_t)&kstack;

  // Load GDT and TSS
  load_gdt(&gp);
  load_tss(5);
}
