#pragma once

#include <hilbert/kernel/vfile.hpp>
#include <stdint.h>

//TODO: end application, threading.

namespace hilbert::kernel::application {

  class process;
  class thread;

  enum class thread_state {
    running,
    paused,
    waiting
  };

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

  enum class seek_origin {
    beginning,
    end,
    current_position
  };

  class stream {
  public:
    virtual ~stream() {}
    virtual stream_result seek(seek_origin origin, int64_t offset) = 0;
    virtual stream_result read(uint64_t count, void *into) = 0;
    virtual stream_result write(uint64_t count, const void *from) = 0;
    virtual stream_result get_length(uint64_t &out) = 0;
    virtual stream_result set_length(uint64_t to) = 0;
  };

  class vfile_stream : public stream {

  private:
    vfile::vfile file;
    uint64_t offset;

  public:
    vfile_stream(vfile::vfile &&file);
    virtual stream_result seek(seek_origin origin, int64_t offset) override;
    virtual stream_result read(uint64_t count, void *into) override;
    virtual stream_result write(uint64_t count, const void *from) override;
    virtual stream_result get_length(uint64_t &out) override;
    virtual stream_result set_length(uint64_t to) override;

  };

  struct socket {
    utility::queue<thread *> process_a_threads_waiting_to_read;
    utility::queue<thread *> process_b_threads_waiting_to_read;
    utility::queue<uint8_t> a_to_b;
    utility::queue<uint8_t> b_to_a;
    bool a_closed;
    bool b_closed;
  };

  class socket_stream : public stream {

  private:
    socket *sock;
    bool are_we_b;

    utility::queue<thread *> &our_threads_waiting_to_read;
    utility::queue<thread *> &their_threads_waiting_to_read;
    utility::queue<uint8_t> &them_to_us;
    utility::queue<uint8_t> &us_to_them;
    bool &them_closed;
    bool &us_closed;

  public:
    socket_stream(socket *sock, bool are_we_b);
    ~socket_stream();
    virtual stream_result seek(seek_origin origin, int64_t offset) override;
    virtual stream_result read(uint64_t count, void *into) override;
    virtual stream_result write(uint64_t count, const void *from) override;
    virtual stream_result get_length(uint64_t &out) override;
    virtual stream_result set_length(uint64_t to) override;

  };

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

  struct socket_listener {
    utility::string id;
    utility::queue<thread *> waiting_to_accept_connection;
    utility::queue<thread *> waiting_to_connect;
    bool is_listening;
  };

  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;

  };

  struct thread {

    //propogated to process on destruction if this is the last thread.
    int exit_code;
    ~thread();

    process *the_process;
    thread_state state;
    //only valid if paused or waiting
    cpu_state cpu;

    stream *just_connected_to;
    stream *just_accepted;

  };

  struct process {

    void end_process(unsigned exit_code);
    void cleanup();

    utility::list<thread *> threads;
    utility::vector<string_pair> environment;
    utility::id_allocator<stream *> open_streams;
    utility::id_allocator<socket_listener *> socket_listeners;

    uint64_t *p4;
    uint64_t *p3;
    uint64_t *p2s[512];
    uint64_t **p1s[512];

    bool **p1es_to_free_on_exit[512];

    uint64_t p4_paddr;

    //set to 0 if none
    uint64_t framebuffer_vaddr;

    //only valid if there are no threads
    int32_t exit_code;

    process();

    //vaddr and paddr must be aligned, and vaddr must be < 0x0080.0000.0000
    void map_page(uint64_t vaddr, uint64_t paddr,
      bool write, bool execute, bool free_pram_on_exit);

    //where "owned" means in lower half and marked free on exit
    bool is_page_owned(uint64_t vaddr);

    //returns start of first page
    uint64_t get_free_vaddr_pages(uint64_t count);

    //in lower half
    uint64_t count_mapped_vram_pages();

  };

  extern utility::id_allocator<process *> *processes;
  extern utility::queue<thread *> *paused_threads;
  extern utility::queue<thread *> *threads_waiting_for_input;
  extern thread *running_thread;
  extern utility::list<socket_listener *> *all_socket_listeners;

  stream_result create_application(
    const vfile::vfile &file, process *&process_out, thread *&thread_out);

  void init_applications();

  //returns true when resumed, false right now.
  //must be called from non-interruptable syscall context.
  extern "C" bool save_thread_state(cpu_state &into);

  //must be called from non-interruptable context
  [[noreturn]] void resume_next();

}
