#include <hilbert/kernel/application.hpp>
#include <hilbert/kernel/utility.hpp>
#include <hilbert/kernel/input.hpp>

namespace hilbert::kernel::application {

  //returns pointer to copy
  extern "C" void *copy_syscall_stack(const uint8_t *bottom) {
    uint64_t len = 0xfffffffffffff000 - (uint64_t)bottom;
    uint8_t *buffer = new uint8_t[len];
    for (uint64_t i = 0; i < len; ++i)
      buffer[i] = bottom[i];
    return buffer;
  }

  extern "C" void restore_syscall_stack(uint8_t *from, uint8_t *to) {
    uint64_t len = 0xfffffffffffff000 - (uint64_t)to;
    for (uint64_t i = 0; i < len; ++i)
      to[i] = from[i];
    delete[] from;
  }

  utility::id_allocator<process *> *all_processes;
  utility::queue<thread *> *paused_threads;
  thread *running_thread;

  struct socket_listener_registration {
    socket_listener *listener;
    utility::string id;
  };

  utility::list<socket_listener_registration> *all_running_socket_listeners;

  extern "C" void init_applications_asm();

  void init_applications() {
    all_processes = new utility::id_allocator<process *>();
    paused_threads = new utility::queue<thread *>();
    running_thread = 0;
    all_running_socket_listeners =
      new utility::list<socket_listener_registration>();
    init_applications_asm();
  }

  uint64_t add_process(process *p) {
    return p->id = all_processes->add_new(utility::move(p));
  }

  socket_listener *try_register_socket_listener(const utility::string &id) {
    for (auto *n = all_running_socket_listeners->first; n; n = n->next)
      if (n->value.id == id)
        return 0;
    socket_listener *sl = new socket_listener();
    all_running_socket_listeners->insert_end({.listener = sl, .id = id});
    return sl;
  }

  socket_listener *try_get_socket_listener(const utility::string &id) {
    for (auto *n = all_running_socket_listeners->first; n; n = n->next)
      if (n->value.id == id)
        return n->value.listener;
    return 0;
  }

  void remove_socket_listener(socket_listener *sl) {
    for (auto *n = all_running_socket_listeners->first; n; n = n->next)
      if (n->value.listener == sl) {
        all_running_socket_listeners->remove(n);
        delete sl;
        return;
      }
  }

  //cpu argument not on stack.
  extern "C" [[noreturn]] void resume_thread(const cpu_state &cpu);

  extern "C" [[noreturn]] void resume_next_thread() {
    running_thread = 0;
    while (paused_threads->count == 0)
      asm volatile ("sti\nhlt\ncli");
    thread *t = paused_threads->take();
    running_thread = t;
    resume_thread(t->saved_state);
  }

  process::process(app_memory *memory, const utility::string &name)
  : name(name), memory(memory) {}

  process::~process() {
    delete memory; //:p
  }

  void process::add_environment_variable(
    utility::string &&name, utility::string &&value) {
    environment_variables.insert_end({.a = name, .b = value});
  }

  utility::string *process::get_environment_variable(
    const utility::string &name) {
    for (auto *i = environment_variables.first; i; i = i->next)
      if (i->value.a == name)
        return &i->value.b;
    return 0;
  }

  void process::add_thread(thread *t) {
    threads.insert_end(t);
  }

  void process::notify_thread_ended(thread *t, int exit_code) {
    threads.remove_first_of(t);
    if (threads.first == 0)
      on_end_process(exit_code);
  }

  void process::on_end_process(int exit_code) {

    while (threads.first) {
      threads.first->value->on_end_thread();
      delete threads.first->value;
      threads.remove(threads.first);
    }

    //TODO: destroy file streams
    //TODO: destroy socket streams
    //TODO: destroy socket listeners

    this->exit_code = exit_code;

  }

  bool process::has_ended() const {
    return threads.first == 0;
  }

  unsigned process::add_file_stream(file_stream *stream) {
    return open_streams.add_new({
      .is_socket = false, .as_file_stream = stream });
  }

  unsigned process::add_socket_stream(socket_stream_end *stream) {
    return open_streams.add_new({
      .is_socket = true, .as_socket_stream = stream });
  }

  unsigned process::add_socket_listener(socket_listener *sl) {
    return running_socket_listeners.add_new(utility::move(sl));
  }

  generic_stream_ptr process::get_stream(unsigned handle) {
    if (open_streams.has_id(handle))
      return open_streams.get(handle);
    return null_gsp;
  }

  generic_stream_ptr process::take_stream(unsigned handle) {
    auto ptr = open_streams.get(handle);
    open_streams.remove_id(handle);
    return ptr;
  }

  void process::add_stream_with_handle(
    unsigned handle, generic_stream_ptr ptr) {
    open_streams.add_id(new generic_stream_ptr(ptr), handle);
  }

  socket_listener *process::get_socket_listener(unsigned handle) {
    if (running_socket_listeners.has_id(handle))
      return running_socket_listeners.get(handle);
    return 0;
  }

  socket_listener *process::take_socket_listener(unsigned handle) {
    if (running_socket_listeners.has_id(handle)) {
      socket_listener *listener = running_socket_listeners.get(handle);
      running_socket_listeners.remove_id(handle);
      return listener;
    }
    return 0;
  }

  void process::close_stream(unsigned handle) {

    if (!open_streams.has_id(handle))
      return;
    auto ptr = open_streams.get(handle);
    open_streams.remove_id(handle);

    if (ptr.is_socket) {
      auto stream = ptr.as_socket_stream;

      if (stream->is_other_side_open) {
        stream->other_end->is_other_side_open = false;
        auto &q = stream->other_end->waiting_to_read;
        while (q.count > 0)
          paused_threads->insert(q.take());
      }

      else
        delete stream->the_socket;

      delete stream;
    }

  }

  thread::thread(process *owner, uint64_t entry)
  : stack_top(owner->memory->map_new_stack()), waiting_for_socket_stream(0),
    waiting_to_accept_from(0), waiting_to_connect_to(0), waiting_for_input(false),
    name(utility::string("main", 4)), owner(owner) {

    saved_state.rax = 0;
    saved_state.rbx = 0;
    saved_state.rcx = 0;
    saved_state.rdx = 0;
    saved_state.rdi = 0;
    saved_state.rsi = 0;
    saved_state.rbp = 0;
    saved_state.rsp = stack_top;
    saved_state.r8 = 0;
    saved_state.r9 = 0;
    saved_state.r10 = 0;
    saved_state.r11 = 0;
    saved_state.r12 = 0;
    saved_state.r13 = 0;
    saved_state.r14 = 0;
    saved_state.r15 = 0;

    saved_state.rflags = 0x200;
    saved_state.rip = entry;
    saved_state.cr3 = owner->memory->p4_paddr;
    saved_state.in_syscall = false;

  }

  void thread::on_end_thread() {
    owner->memory->unmap_stack(stack_top);
    if (waiting_for_socket_stream)
      waiting_for_socket_stream->waiting_to_read.remove(this);
    else if (waiting_to_accept_from)
      waiting_to_accept_from->waiting_to_accept.remove(this);
    else if (waiting_to_connect_to)
      waiting_to_connect_to->waiting_to_connect.remove(this);
    else if (waiting_for_input)
      input::waiting_for_input->remove(this);
  }

  void thread::wait_for_socket_stream(socket_stream_end *the_socket_stream) {
    waiting_for_socket_stream = the_socket_stream;
    the_socket_stream->waiting_to_read.insert(this);
    yield(saved_state);
    waiting_for_socket_stream = 0;
  }

  utility::maybe<unsigned> thread::wait_to_accept_from(
    socket_listener *the_socket_listener) {
    waiting_to_accept_from = the_socket_listener;
    the_socket_listener->waiting_to_accept.insert(this);
    yield(saved_state);
    waiting_to_accept_from = 0;
    return new_socket_stream_id;
  }

  utility::maybe<unsigned> thread::wait_to_connect_to(
    socket_listener *the_socket_listener) {
    waiting_to_connect_to = the_socket_listener;
    the_socket_listener->waiting_to_connect.insert(this);
    yield(saved_state);
    waiting_to_connect_to = 0;
    return new_socket_stream_id;
  }

  void thread::wait_for_input() {
    waiting_for_input = true;
    input::waiting_for_input->insert(this);
    yield(saved_state);
    waiting_for_input = false;
  }

  void thread::notify_no_socket_stream() {
    new_socket_stream_id.has_value = false;
    paused_threads->insert(this);
  }

  void thread::notify_new_socket_stream(unsigned id) {
    new_socket_stream_id.has_value = true;
    new_socket_stream_id.value = id;
    paused_threads->insert(this);
  }

}
