#include <hilbert/kernel/framebuffer.hpp>
#include <hilbert/kernel/serial.hpp>
#include <hilbert/kernel/input.hpp>
#include <hilbert/kernel/panic.hpp>

using namespace hilbert::kernel;

struct [[gnu::packed]] exception_info_t {

  uint64_t rax;
  uint64_t rbx;
  uint64_t rcx;
  uint64_t rdx;
  uint64_t rdi;
  uint64_t rsi;
  uint64_t rbp;
  uint64_t rsp;
  uint64_t r8;
  uint64_t r9;
  uint64_t r10;
  uint64_t r11;
  uint64_t r12;
  uint64_t r13;
  uint64_t r14;
  uint64_t r15;

  uint64_t cr2;
  uint64_t cr3;
  uint64_t rip;
  uint64_t rflags;

  uint64_t error;
  uint8_t has_error;//0 or 1
  uint8_t exception_number;

};

extern exception_info_t exception_info;

static inline void print_reg(const char *regname, uint64_t value) {
  serial_putstr("  ");
  serial_putstr(regname);
  serial_putstr(" = 0x");
  serial_puthex<16>(value);
  serial_putchar('\n');
}

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

  //so exception_info's type is known by gdb
  exception_info_t the_exception_info = exception_info;
  (void)the_exception_info;

  serial_putstr("exception 0x");
  serial_puthex<2>(exception_info.exception_number);
  serial_putchar('\n');

  if (exception_info.has_error)
    print_reg("error code", exception_info.error);

  print_reg("rflags", exception_info.rflags);
  print_reg("rip", exception_info.rip);
  print_reg("cr2", exception_info.cr2);
  print_reg("cr3", exception_info.cr3);
  print_reg("rax", exception_info.rax);
  print_reg("rbx", exception_info.rbx);
  print_reg("rcx", exception_info.rcx);
  print_reg("rdx", exception_info.rdx);
  print_reg("rdi", exception_info.rdi);
  print_reg("rsi", exception_info.rsi);
  print_reg("rbp", exception_info.rbp);
  print_reg("rsp", exception_info.rsp);
  print_reg("r8 ", exception_info.r8 );
  print_reg("r9 ", exception_info.r9 );
  print_reg("r10", exception_info.r10);
  print_reg("r11", exception_info.r11);
  print_reg("r12", exception_info.r12);
  print_reg("r13", exception_info.r13);
  print_reg("r14", exception_info.r14);
  print_reg("r15", exception_info.r15);

  if (application::running_thread != 0) {
    serial_putstr("running app = ");
    serial_putstr(application::running_thread->owner->name);
  }

  panic(0xba40bb);

}

static uint32_t current_flags = 0;

#define SETBIT(field, bit, cond) \
  field = (cond) ? (field | (bit)) : (field & ~(bit));

static void got_key(uint32_t key) {

  input::input_queue->insert({
    .keyboard = current_flags | key, .is_mouse = false});

  input::notify_waiting();

  if (key == (input::BREAK | 0x77))
    current_flags ^= input::NUM_LOCK;

  else if (key == (input::BREAK | 0x58))
    current_flags ^= input::CAPS_LOCK;

  else if ((key & 0xff) == 0xa7)
    SETBIT(current_flags, input::RIGHT_WIN, !(key & input::BREAK))

  else if ((key & 0xff) == 0x9f)
    SETBIT(current_flags, input::LEFT_WIN, !(key & input::BREAK))

  else if ((key & 0xff) == 0x91)
    SETBIT(current_flags, input::RIGHT_ALT, !(key & input::BREAK))

  else if ((key & 0xff) == 0x11)
    SETBIT(current_flags, input::LEFT_ALT, !(key & input::BREAK))

  else if ((key & 0xff) == 0x94)
    SETBIT(current_flags, input::RIGHT_CTRL, !(key & input::BREAK))

  else if ((key & 0xff) == 0x14)
    SETBIT(current_flags, input::LEFT_CTRL, !(key & input::BREAK))

  else if ((key & 0xff) == 0x59)
    SETBIT(current_flags, input::RIGHT_SHIFT, !(key & input::BREAK))

  else if ((key & 0xff) == 0x12)
    SETBIT(current_flags, input::LEFT_SHIFT, !(key & input::BREAK))

}

static uint8_t key_so_far[8];
static uint8_t key_so_far_len = 0;

extern "C" void on_keyboard_interrupt(uint8_t byte) {

  key_so_far[key_so_far_len++] = byte;

  if (key_so_far_len == 1) {
    if (byte != 0xe0 && byte != 0xe1 && byte != 0xf0) {
      got_key(byte);
      key_so_far_len = 0;
    }
  }

  else if (key_so_far_len == 2) {
    if (key_so_far[0] == 0xe0 && byte != 0xf0 && byte != 0x12) {
      got_key(byte | 0x80);
      key_so_far_len = 0;
    }
    else if (key_so_far[0] == 0xf0) {
      got_key(input::BREAK | byte);
      key_so_far_len = 0;
    }
  }

  else if (key_so_far_len == 3) {
    if (key_so_far[0] == 0xe0 && key_so_far[1] == 0xf0 && byte != 0x7c) {
      got_key(input::BREAK | byte | 0x80);
      key_so_far_len = 0;
    }
  }

  else if (key_so_far_len == 4) {
    if (key_so_far[0] == 0xe0 && key_so_far[1] == 0x12 &&
        key_so_far[2] == 0xe0 && byte == 0x7c) {
      got_key(0xe0);
      key_so_far_len = 0;
    }
  }

  else if (key_so_far_len == 6) {
    if (key_so_far[0] == 0xe0 && key_so_far[1] == 0xf0 &&
        key_so_far[2] == 0x7c && key_so_far[3] == 0xe0 &&
        key_so_far[4] == 0xf0 && byte == 0x12) {
      got_key(input::BREAK | 0xe0);
      key_so_far_len = 0;
    }
  }

  else if (key_so_far_len == 8) {
    if (key_so_far[0] == 0xe1 && key_so_far[1] == 0x14 &&
        key_so_far[2] == 0x77 && key_so_far[3] == 0xe1 &&
        key_so_far[2] == 0xf0 && key_so_far[3] == 0x14 &&
        key_so_far[4] == 0xf0 && byte == 0x77) {
      got_key(0xe1);
      got_key(input::BREAK | 0xe1);
    }
    key_so_far_len = 0;
  }

}

static uint8_t mouse_packet_so_far[3];
static uint8_t mouse_packet_so_far_len = 0;

extern "C" void on_mouse_interrupt(uint8_t byte) {

  if (mouse_packet_so_far_len == 0 && byte == 0xfa)
    //dirty hack
    return;

  mouse_packet_so_far[mouse_packet_so_far_len++] = byte;
  if (mouse_packet_so_far_len < 3)
    return;

  mouse_packet_so_far_len = 0;

  int16_t x_change = mouse_packet_so_far[1];
  int16_t y_change = -(int16_t)mouse_packet_so_far[2];
  if (mouse_packet_so_far[0] & 0x10)
    x_change -= 0x100;
  if (mouse_packet_so_far[0] & 0x20)
    y_change += 0x100;

  input::input_packet packet = {
    .mouse = {
      .x_change = x_change, .y_change = y_change,
      .buttons = (input::buttons_t)(mouse_packet_so_far[0] & 7) },
    .is_mouse = true };

  if (input::input_queue->count > 0 &&
      input::input_queue->last_inserted().is_mouse &&
      input::input_queue->last_inserted().mouse.buttons ==
      packet.mouse.buttons) {
    input::input_queue->last_inserted().mouse.x_change +=
      packet.mouse.x_change;
    input::input_queue->last_inserted().mouse.y_change +=
      packet.mouse.y_change;
  }
  else
    input::input_queue->insert(packet);

  input::notify_waiting();

}