#include <hilbert/kernel/application.hpp>
#include <hilbert/kernel/framebuffer.hpp>
#include <hilbert/kernel/paging.hpp>
#include <hilbert/kernel/input.hpp>
#include <hilbert/kernel/panic.hpp>
#include <hilbert/kernel/vfile.hpp>

namespace hilbert::kernel::syscall {

  enum file_result : uint64_t {
    file_result_success,
    file_result_bad_file_handle,
    file_result_device_error,
    file_result_file_system_corrupt,
    file_result_out_of_bounds,
    file_result_does_not_exist,
    file_result_directory
  };

  bool is_range_owned_by_application(uint64_t start, uint64_t end) {
    auto *process = application::running_thread->the_process;
    uint64_t pstart = (start / 4096) * 4096;
    uint64_t pend = ((end - 1) / 4096 + 1) * 4096;
    for (uint64_t p = pstart; p < pend; p += 4096)
      if (!process->is_page_owned(p))
        return false;
    return true;
  }

  void set_zero(uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx) {
    rax = 0;
    rdi = 0;
    rsi = 0;
    rdx = 0;
  }

  void encode_color_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {
    rax = (uint64_t)framebuffer::encode_color(
      rdi & 0xff, (rdi >> 8) & 0xff, (rdi >> 16) & 0xff);
    rdi = 0;
    rsi = 0;
    rdx = 0;
  }

  void get_framebuffer_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    auto *process = application::running_thread->the_process;
    if (process->framebuffer_vaddr == 0) {
      uint64_t pages_needed =
        (framebuffer::dword_pitch * framebuffer::height * 4 - 1) / 4096 + 1;
      uint64_t vaddr = process->get_free_vaddr_pages(pages_needed);
      for (uint64_t i = 0; i < pages_needed; ++i)
        process->map_page(
          vaddr + i * 4096, framebuffer::paddr + i * 4096, true, false, false);
      process->framebuffer_vaddr = vaddr;
    }

    rax = process->framebuffer_vaddr;
    rdi =
      (uint64_t)(uint32_t)framebuffer::width |
      ((uint64_t)(uint32_t)framebuffer::height << 32);
    rsi = (uint32_t)framebuffer::dword_pitch;
    rdx = 0;

  }

  void open_file_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    if (!is_range_owned_by_application(rdi, rdi + rsi)) {
      set_zero(rax, rdi, rsi, rdx);
      return;
    }

    utility::string path_string((const char *)rdi, rsi);

    set_zero(rax, rdi, rsi, rdx);

    vfile::canon_path cp;
    vfile::vfile file;
    vfile::canonize_path(path_string, cp);

    switch (vfile::lookup_path(cp, file, true)) {

    case storage::fs_result::device_error:
    case storage::fs_result::fs_corrupt:

      rax = (uint64_t)application::stream_result::io_error;
      return;

    case storage::fs_result::does_not_exist:

      if (!(rdx & 1)) {
        rax = (uint64_t)application::stream_result::does_not_exist;
        return;
      }

      //TODO: create the file
      panic(0x9af5e6);

    case storage::fs_result::success:

      if (rdx & 2) {
        rax = (uint64_t)application::stream_result::already_exists;
        return;
      }

      if (file.dir_entry.type != storage::file_type::regular_file) {
        rax = (uint64_t)application::stream_result::not_a_regular_file;
        return;
      }

      rax = (uint64_t)application::stream_result::success;
      rdi = application::running_thread->the_process->open_streams.add_new(
        new application::vfile_stream(utility::move(file)));

      return;

    }

  }

  void end_this_thread_syscall(
    uint64_t &, uint64_t &rdi, uint64_t &, uint64_t &
  ) {
    application::running_thread->exit_code = (int)(uint32_t)rdi;
    delete application::running_thread;
    application::resume_next();
  }

  void get_new_pages_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    uint64_t count = rdi;
    set_zero(rax, rdi, rsi, rdx);

    auto *p = application::running_thread->the_process;
    uint64_t vaddr = p->get_free_vaddr_pages(count);

    for (uint64_t i = 0; i < count; ++i) {
      uint64_t kvaddr;
      uint64_t paddr;
      paging::map_new_kernel_page(kvaddr, paddr);
      for (int i = 0; i < 4096; ++i)
        ((uint8_t *)kvaddr)[i] = 0;
      paging::unmap_kernel_page((uint64_t)kvaddr);
      p->map_page(vaddr + i * 4096, paddr, true, false, true);
    }

    rax = vaddr;

  }

  void get_input_packet_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    set_zero(rax, rdi, rsi, rdx);
    auto *t = application::running_thread;

    do
      if (input::input_queue->count > 0) {
        input::input_packet packet = input::input_queue->take();
        if (packet.is_mouse) {
          rax = packet.mouse.buttons | 0x80;
          rdi = (uint16_t)packet.mouse.x_change;
          rsi = (uint16_t)packet.mouse.y_change;
        }
        else {
          rax = 0;
          rdi = packet.keyboard;
        }
        return;
      }
    while (application::save_thread_state(t->cpu));

    t->state = application::thread_state::waiting;
    application::threads_waiting_for_input->insert(t);
    application::resume_next();

  }

  void create_private_socket_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {
    auto *s = new application::socket;
    auto *ss1 = new application::socket_stream(s, false);
    auto *ss2 = new application::socket_stream(s, true);
    set_zero(rax, rdi, rsi, rdx);
    auto *p = application::running_thread->the_process;
    rax = (uint64_t)p->open_streams.add_new(ss1);
    rdi = (uint64_t)p->open_streams.add_new(ss2);
  }

  void create_socket_listener_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    if (!is_range_owned_by_application(rdi, rdi + rsi)) {
      set_zero(rax, rdi, rsi, rdx);
      return;
    }

    utility::string id_string((const char *)rdi, rsi);
    set_zero(rax, rdi, rsi, rdx);

    for (auto *p = application::all_socket_listeners->first; p; p = p->next)
      if (p->value->id == id_string) {
        rax = (uint64_t)application::stream_result::socket_id_already_used;
        return;
      }

    auto *sl = new application::socket_listener();
    sl->id = utility::move(id_string);
    sl->is_listening = true;
    rax = (uint64_t)application::stream_result::success;
    rdi = (uint64_t)application::running_thread->the_process
      ->socket_listeners.add_new(utility::move(sl));

  }

  void stop_socket_listener_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    set_zero(rax, rdi, rsi, rdx);
    auto *p = application::running_thread->the_process;

    if (p->socket_listeners.has_id(handle)) {
      auto *sl = p->socket_listeners.get(handle);
      p->socket_listeners.remove_id(handle);
      if (sl->waiting_to_accept_connection.count > 0 ||
          sl->waiting_to_connect.count > 0) {
        sl->is_listening = false;
        while (sl->waiting_to_accept_connection.count > 0) {
          auto *t = sl->waiting_to_accept_connection.take();
          t->state = application::thread_state::paused;
          application::paused_threads->insert(t);
        }
        while (sl->waiting_to_connect.count > 0) {
          auto *t = sl->waiting_to_connect.take();
          t->state = application::thread_state::paused;
          application::paused_threads->insert(t);
        }
      }
      else
        delete sl;
    }

  }

  void accept_socket_connection_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    set_zero(rax, rdi, rsi, rdx);
    auto *t = application::running_thread;
    auto *p = t->the_process;

    if (!p->socket_listeners.has_id(handle)) {
      rax = (uint64_t)application::stream_result::socket_id_not_in_use;
      return;
    }

    auto *sl = p->socket_listeners.get(handle);

    if (sl->waiting_to_connect.count > 0) {
      auto *ot = sl->waiting_to_connect.take();
      auto *sock = new application::socket();
      application::stream *s1 = new application::socket_stream(sock, false);
      application::stream *s2 = new application::socket_stream(sock, true);
      unsigned handle = p->open_streams.add_new(utility::move(s1));
      ot->just_connected_to = s2;
      ot->state = application::thread_state::paused;
      application::paused_threads->insert(ot);
      rax = (uint64_t)application::stream_result::success;
      rdi = handle;
      return;
    }

    if (application::save_thread_state(t->cpu)) {
      if (sl->is_listening) {
        rax = (uint64_t)application::stream_result::success;
        rdi = p->open_streams.add_new(utility::move(t->just_accepted));
      }
      else {
        if (sl->waiting_to_accept_connection.count == 0 &&
            sl->waiting_to_connect.count == 0)
          delete sl;
        rax = (uint64_t)application::stream_result::socket_listener_closed;
      }
      return;
    }

    t->state = application::thread_state::waiting;
    sl->waiting_to_accept_connection.insert(t);
    application::resume_next();

  }

  void connect_to_socket_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    if (!is_range_owned_by_application(rdi, rdi + rsi)) {
      set_zero(rax, rdi, rsi, rdx);
      return;
    }

    utility::string id_string((const char *)rdi, rsi);
    set_zero(rax, rdi, rsi, rdx);

    for (auto *i = application::all_socket_listeners->first; i; i = i->next)
      if (i->value->id == id_string) {
        auto *sl = i->value;
        auto *t = application::running_thread;
        auto *p = t->the_process;

        if (sl->waiting_to_accept_connection.count > 0) {
          auto *ot = sl->waiting_to_accept_connection.take();
          auto *sock = new application::socket();
          auto *s1 = new application::socket_stream(sock, false);
          auto *s2 = new application::socket_stream(sock, true);
          unsigned handle = p->open_streams.add_new(utility::move(s1));
          ot->just_accepted = s2;
          ot->state = application::thread_state::paused;
          application::paused_threads->insert(ot);
          rax = (uint64_t)application::stream_result::success;
          rdi = handle;
          return;
        }

        if (application::save_thread_state(t->cpu)) {
          if (sl->is_listening) {
            rax = (uint64_t)application::stream_result::success;
            rdi = p->open_streams.add_new(utility::move(t->just_connected_to));
          }
          else {
            if (sl->waiting_to_accept_connection.count == 0 &&
                sl->waiting_to_connect.count == 0)
              delete sl;
            rax = (uint64_t)application::stream_result::socket_id_not_in_use;
          }
          return;
        }

        t->state = application::thread_state::waiting;
        sl->waiting_to_connect.insert(t);
        application::resume_next();

      }

    rax = (uint64_t)application::stream_result::socket_id_not_in_use;

  }

  void close_stream_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    set_zero(rax, rdi, rsi, rdx);
    auto *p = application::running_thread->the_process;

    if (p->open_streams.has_id(handle)) {
      application::stream *s = p->open_streams.get(handle);
      p->open_streams.remove_id(handle);
      delete s;
    }

  }

  void seek_stream_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    uint8_t origin = (uint8_t)rsi;
    int64_t offset = (int64_t)rdx;
    set_zero(rax, rdi, rsi, rdx);

    if (origin >= 3)
      return;

    auto *p = application::running_thread->the_process;

    if (!p->open_streams.has_id(handle)) {
      rax = (uint64_t)application::stream_result::bad_handle;
      return;
    }

    rax = (uint64_t)p->open_streams.get(handle)
      ->seek((application::seek_origin)origin, offset);

  }

  void read_from_stream_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    uint64_t count = (uint64_t)rsi;
    uint64_t buffer = (uint64_t)rdx;
    set_zero(rax, rdi, rsi, rdx);

    if (!is_range_owned_by_application(buffer, buffer + count))
      return;

    auto *p = application::running_thread->the_process;

    if (!p->open_streams.has_id(handle)) {
      rax = (uint64_t)application::stream_result::bad_handle;
      return;
    }

    rax = (uint64_t)p->open_streams.get(handle)->read(count, (void *)buffer);

  }

  void write_to_stream_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    uint64_t count = (uint64_t)rsi;
    uint64_t buffer = (uint64_t)rdx;
    set_zero(rax, rdi, rsi, rdx);

    if (!is_range_owned_by_application(buffer, buffer + count))
      return;

    auto *p = application::running_thread->the_process;

    if (!p->open_streams.has_id(handle)) {
      rax = (uint64_t)application::stream_result::bad_handle;
      return;
    }

    rax = (uint64_t)p->open_streams.get(handle)
      ->write(count, (const void *)buffer);

  }

  void get_stream_length_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    set_zero(rax, rdi, rsi, rdx);

    auto *p = application::running_thread->the_process;

    if (!p->open_streams.has_id(handle)) {
      rax = (uint64_t)application::stream_result::bad_handle;
      return;
    }

    rax = (uint64_t)p->open_streams.get(handle)->get_length(rdi);

  }

  void start_process_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {
    //TODO
    (void)rax;
    (void)rdi;
    (void)rsi;
    (void)rdx;
    panic(0x9af5e6);
  }

  void end_this_process_syscall(
    uint64_t &, uint64_t &rdi, uint64_t &, uint64_t &
  ) {
    application::running_thread->the_process->end_process((unsigned)rdi);
    application::resume_next();
  }

  void set_stream_length_syscall(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
  ) {

    unsigned handle = (unsigned)rdi;
    uint64_t new_length = rsi;
    set_zero(rax, rdi, rsi, rdx);

    auto *p = application::running_thread->the_process;

    if (!p->open_streams.has_id(handle)) {
      rax = (uint64_t)application::stream_result::bad_handle;
      return;
    }

    rax = (uint64_t)p->open_streams.get(handle)->set_length(new_length);

  }

  typedef void (*syscall_handler)(
    uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx);

  syscall_handler handlers[] = {
    &encode_color_syscall,
    &get_framebuffer_syscall,
    &open_file_syscall,
    &end_this_thread_syscall,
    &get_new_pages_syscall,
    &get_input_packet_syscall,
    &create_private_socket_syscall,
    &create_socket_listener_syscall,
    &stop_socket_listener_syscall,
    &accept_socket_connection_syscall,
    &connect_to_socket_syscall,
    &close_stream_syscall,
    &seek_stream_syscall,
    &read_from_stream_syscall,
    &write_to_stream_syscall,
    &get_stream_length_syscall,
    &start_process_syscall,
    &end_this_process_syscall,
    &set_stream_length_syscall
  };

  static constexpr int max_syscall_number = 19;

}

using namespace hilbert::kernel::syscall;

extern "C" void do_syscall(
  uint64_t &rax, uint64_t &rdi, uint64_t &rsi, uint64_t &rdx
) {

  if (rax <= max_syscall_number && handlers[rax] != 0)
    handlers[rax](rax, rdi, rsi, rdx);
  else
    set_zero(rax, rdi, rsi, rdx);

}
