#pragma once

#include <hilbert/kernel/app-memory.hpp>
#include <hilbert/kernel/utility.hpp>
#include <hilbert/kernel/vfile.hpp>
#include <stdint.h>

namespace hilbert::kernel::application {

  enum class stream_result {
    success = 0,
    bad_handle,
    io_error,
    out_of_bounds,
    does_not_exist,
    not_a_regular_file,
    not_an_executable,
    not_writable,
    not_seekable,
    socket_id_already_in_use,
    socket_id_not_in_use,
    socket_listener_closed,
    other_end_closed,
    already_exists,
    not_sized,
    not_readable
  };

  struct [[gnu::packed]] cpu_state {

    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 rflags;
    uint64_t rip;
    uint64_t cr3;

    //only used if in_syscall is true. starts at rsp. no red zone
    //needs to be saved since save_thread_state doesn't use it.
    void *kernel_stack_copy;
    bool in_syscall;

  };

  class process;
  class thread;
  struct file_stream;
  struct socket;
  struct socket_stream_end;
  struct socket_listener;

  struct generic_stream_ptr {
    bool is_socket;
    union {
      file_stream *as_file_stream;
      socket_stream_end *as_socket_stream;
    };
    bool is_null() {
      return !is_socket && as_file_stream == 0;
    }
  };

  constexpr generic_stream_ptr null_gsp {
    .is_socket = false, .as_file_stream = 0
  };

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

  void init_applications();
  uint64_t add_process(process *p);

  //returns new listener on success, 0 on failure. id is copied.
  socket_listener *try_register_socket_listener(const utility::string &id);
  //returns previously registered listener on success, 0 on failure.
  socket_listener *try_get_socket_listener(const utility::string &id);
  //this deletes the passed object as well.
  void remove_socket_listener(socket_listener *sl);

  //saves running thread state, and switches to new task. when the thread is
  //resumed, returns to caller.
  extern "C" void yield(cpu_state &save_state_into);

  extern "C" [[noreturn]] void resume_next_thread();

  class process {

    utility::list<thread *> threads;
    utility::id_allocator<generic_stream_ptr> open_streams;
    utility::id_allocator<socket_listener *> running_socket_listeners;

    struct string_pair {
      utility::string a;
      utility::string b;
    };

    utility::list<string_pair> environment_variables;

  public:
    utility::string name;

    app_memory *memory;

    //set in get_framebuffer syscall
    uint64_t framebuffer_vaddr = 0;

    //set by add_process
    uint64_t id;

    //this class takes ownership of memory
    process(app_memory *memory, const utility::string &name);
    ~process();

    //arguments are utility::move'd
    void add_environment_variable(
      utility::string &&name, utility::string &&value);

    //null if unset
    utility::string *get_environment_variable(const utility::string &name);

    void add_thread(thread *t);
    void notify_thread_ended(thread *t, int exit_code);
    void on_end_process(int exit_code);
    bool has_ended() const;

    //these return handles
    unsigned add_file_stream(file_stream *stream);
    unsigned add_socket_stream(socket_stream_end *sse);
    unsigned add_socket_listener(socket_listener *sl);

    //if doesn't exist, returns null_gsp.
    generic_stream_ptr get_stream(unsigned handle);

    //assumes exists. just removes it from here
    //and returns it. does no other cleanup
    generic_stream_ptr take_stream(unsigned handle);

    //assumes handle is not in use
    void add_stream_with_handle(unsigned handle, generic_stream_ptr ptr);

    //returns 0 if does not exist.
    socket_listener *get_socket_listener(unsigned handle);

    //just removes it from the list in here and returns it.
    //does no other cleanup. returns 0 if does not exist.
    socket_listener *take_socket_listener(unsigned handle);

    void close_stream(unsigned handle);

    //only meaningful if has_ended() is true
    int exit_code;

  };

  class thread {

    uint64_t stack_top;

    //these four are set to 0 / false if the thread is not waiting.
    //at most one of these can be non-zero / true at any time.
    socket_stream_end *waiting_for_socket_stream;
    socket_listener *waiting_to_accept_from;
    socket_listener *waiting_to_connect_to;
    bool waiting_for_input;

    //this is only meaningful if the thread has just awoken from waiting to
    //accept a connection from or connect to a socket stream. if the attempt
    //was successful, new_socket_stream is set to the id of our end. otherwise,
    //it is empty.
    utility::maybe<unsigned> new_socket_stream_id;

  public:
    utility::string name;

    process *owner;

    //the cpu state is saved here when the thread is not running.
    cpu_state saved_state;

    thread(process *owner, uint64_t entry);
    //also call owner->notify_thread_ended, unless this
    //is being called from process::on_end_process
    void on_end_thread();

    //these set up the necessary variables here and in the other object, save
    //the cpu state, and then call resume_next. when the process is awoken by
    //the other object, these return to their caller.
    void wait_for_file_stream(file_stream *the_file_stream);
    void wait_for_socket_stream(socket_stream_end *the_socket_stream);
    utility::maybe<unsigned> wait_to_accept_from(
      socket_listener *the_socket_listener);
    utility::maybe<unsigned> wait_to_connect_to(
      socket_listener *the_socket_listener);
    void wait_for_input();

    //these set the relevant variables and then wake up the thread
    void notify_no_socket_stream();
    void notify_new_socket_stream(unsigned id);

  };

  struct file_stream {
    vfile::vfile the_file;
    uint64_t offset;
  };

  struct socket {
    utility::queue<uint8_t> queue_1;
    utility::queue<uint8_t> queue_2;
  };

  struct socket_stream_end {
    socket *the_socket;
    utility::queue<uint8_t> &read_queue;
    utility::queue<uint8_t> &write_queue;
    utility::queue<thread *> waiting_to_read;
    bool is_other_side_open;
    process *other_process;
    socket_stream_end *other_end;
  };

  struct socket_listener {
    utility::queue<thread *> waiting_to_accept;
    utility::queue<thread *> waiting_to_connect;
  };

}
