#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <fuse.h>

#define WBY_STATIC
#define WBY_IMPLEMENTATION
#define WBY_USE_FIXED_TYPES
#define WBY_USE_ASSERT
#include "mmx/web.h"

#include "cJSON/cJSON.h"
#include "cJSON/cJSON.c"

struct wby_server server;
struct wby_con *con = NULL;

pthread_mutex_t request_data_mutex = PTHREAD_MUTEX_INITIALIZER;
char *request_data = NULL;

pthread_cond_t response_cv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t response_mutex = PTHREAD_MUTEX_INITIALIZER;
cJSON *response = NULL;

static const char  *file_path      = "/hello.txt";
static const char   file_content[] = "Hello World!\n";
static const size_t file_size      = sizeof(file_content)/sizeof(char) - 1;

static void dispatch_send_req(cJSON *req) {
  pthread_mutex_lock(&request_data_mutex);

  request_data = cJSON_Print(req);
  printf("%s\n", request_data);

  pthread_mutex_unlock(&request_data_mutex);
}

void send_req_if_any() {
  pthread_mutex_lock(&request_data_mutex);

  if (con == NULL || request_data == NULL) goto done;

  wby_frame_begin(con, WBY_WSOP_TEXT_FRAME);
  wby_write(con, request_data, strlen(request_data));
  wby_frame_end(con);

  free(request_data);
  request_data = NULL;

 done:
  pthread_mutex_unlock(&request_data_mutex);
}

static cJSON *await_response() {
  pthread_mutex_lock(&response_mutex);

  response = NULL;
  while (response == NULL) {
    pthread_cond_wait(&response_cv, &response_mutex);
  }

  cJSON *resp = response;
  pthread_mutex_unlock(&response_mutex);

  return resp;
}

#define MAKE_REQ(op, req_body, resp_handler) \
  do { \
    int ret = -1;                                     \
    cJSON *req = NULL;                              \
    cJSON *resp = NULL;                           \
                                                  \
    pthread_mutex_lock(&request_data_mutex); \
    int disconnected = (con == NULL); \
    pthread_mutex_unlock(&request_data_mutex); \
    if (disconnected) { ret = -EIO; goto done; }        \
    \
    req = cJSON_CreateObject(); \
    cJSON_AddStringToObject(req, "op", op);        \
    req_body \
    \
    dispatch_send_req(req); \
    \
    resp = await_response();\
    \
    cJSON *error_item = cJSON_GetObjectItemCaseSensitive(resp, "error"); \
    if (error_item) { \
      ret = -error_item->valueint; \
      if (ret != 0) goto done; \
    } \
    \
    ret = -1; \
    resp_handler \
    \
done: \
    if (req != NULL) cJSON_Delete(req); \
    if (resp != NULL) cJSON_Delete(resp); \
    return ret;                               \
  } while (0)

#define JSON_GET_PROP_INT(lvalue, key) \
  do { \
    lvalue = cJSON_GetObjectItemCaseSensitive(resp, key)->valueint;     \
  } while (0)

static int
hello_getattr(const char *path, struct stat *stbuf)
{
    memset(stbuf, 0, sizeof(struct stat));
    printf("\n\ngetattr(%s)\n", path);

    MAKE_REQ("getattr", {
        cJSON_AddStringToObject(req, "path", path);
    }, {
        JSON_GET_PROP_INT(stbuf->st_mode, "st_mode");
        JSON_GET_PROP_INT(stbuf->st_nlink, "st_nlink");
        JSON_GET_PROP_INT(stbuf->st_size, "st_size");
        printf("returning re getattr(%s)\n", path);

        ret = 0;
    });
}

static int
hello_open(const char *path, struct fuse_file_info *fi)
{
    MAKE_REQ("open", {
        cJSON_AddStringToObject(req, "path", path);
        cJSON_AddNumberToObject(req, "flags", fi->flags);
    }, {
        cJSON *fh_item = cJSON_GetObjectItemCaseSensitive(resp, "fh");
        if (fh_item) fi->fh = fh_item->valueint;

        ret = 0;
    });
}

static int
hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
              off_t offset, struct fuse_file_info *fi)
{
    printf("\n\nreaddir(%s)\n", path);
    
    // send {op: "readdir", path} to the websocket handler
    MAKE_REQ("readdir", {
        cJSON_AddStringToObject(req, "path", path);
    }, {
        cJSON *entries = cJSON_GetObjectItemCaseSensitive(resp, "entries");
        cJSON *entry;
        cJSON_ArrayForEach(entry, entries) {
            filler(buf, cJSON_GetStringValue(entry), NULL, 0);
            printf("entry: [%s]\n", cJSON_GetStringValue(entry));
        }

        ret = 0;
    });
}

static int
hello_read(const char *path, char *buf, size_t size, off_t offset,
           struct fuse_file_info *fi)
{
    MAKE_REQ("read", {
        cJSON_AddStringToObject(req, "path", path);
        cJSON_AddNumberToObject(req, "size", size);
        cJSON_AddNumberToObject(req, "offset", offset);

        cJSON_AddNumberToObject(req, "fh", fi->fh);
        cJSON_AddNumberToObject(req, "flags", fi->flags);
    }, {
        size_t resp_size;
        JSON_GET_PROP_INT(resp_size, "size");
        size = resp_size < size ? resp_size : size;

        cJSON *resp_buf_item = cJSON_GetObjectItemCaseSensitive(resp, "buf");
        char *resp_buf = cJSON_GetStringValue(resp_buf_item);
        size_t resp_buf_len = strlen(resp_buf);
        size = resp_buf_len < size ? resp_buf_len : size;

        memcpy(buf, resp_buf, size);

        ret = size;
    });
}

static int hello_release(const char *path, struct fuse_file_info *fi) {
    MAKE_REQ("release", {
        cJSON_AddStringToObject(req, "path", path);
        cJSON_AddNumberToObject(req, "fh", fi->fh);
    }, {
        ret = 0;
    });
}

static struct fuse_operations hello_filesystem_operations = {
    .getattr = hello_getattr, /* To provide size, permissions, etc. */
    .open    = hello_open,    /* To enforce read-only access.       */
    .read    = hello_read,    /* To provide file content.           */
    .release = hello_release,
    .readdir = hello_readdir, /* To provide directory listing.      */
};

static int
dispatch(struct wby_con *connection, void *userdata)
{
    return 1;
}

static int
websocket_connect(struct wby_con *connection, void *userdata)
{
    /* connection bound userdata */
    connection->user_data = NULL;
    if (0 == strcmp(connection->request.uri, "/"))
        return 0;
    return 1;
}

static void
websocket_connected(struct wby_con *connection, void *userdata)
{
    printf("WebSocket connected\n");
    con = connection;
}

static int
websocket_frame(struct wby_con *connection, const struct wby_frame *frame, void *userdata)
{
    unsigned char data[131072] = {0};

    int i = 0;
    printf("WebSocket frame incoming\n");
    printf("  Frame OpCode: %d\n", frame->opcode);
    printf("  Final frame?: %s\n", (frame->flags & WBY_WSF_FIN) ? "yes" : "no");
    printf("  Masked?     : %s\n", (frame->flags & WBY_WSF_MASKED) ? "yes" : "no");
    printf("  Data Length : %d\n", (int) frame->payload_length);

    if ((unsigned long) frame->payload_length > sizeof(data)) {
        printf("Data too long!\n");
        exit(1);
    }
    
    while (i < frame->payload_length) {
        unsigned char buffer[16];
        int remain = frame->payload_length - i;
        size_t read_size = remain > (int) sizeof buffer ? sizeof buffer : (size_t) remain;
        size_t k;

        printf("%08x ", (int) i);
        if (0 != wby_read(connection, buffer, read_size))
            break;
        for (k = 0; k < read_size; ++k)
            printf("%02x ", buffer[k]);
        for (k = read_size; k < 16; ++k)
            printf("   ");
        printf(" | ");
        for (k = 0; k < read_size; ++k)
            printf("%c", isprint(buffer[k]) ? buffer[k] : '?');
        printf("\n");
        for (k = 0; k < read_size; ++k)
          data[i + k] = buffer[k];
        i += (int)read_size;
    }

    if ((int) strlen((const char *) data) != frame->payload_length) {
      printf("Null in data! [%s]\n", data);
    }

    pthread_mutex_lock(&response_mutex);
    response = cJSON_Parse((const char *) data);
    pthread_cond_signal(&response_cv);
    pthread_mutex_unlock(&response_mutex);

    return 0;
}

static void
websocket_closed(struct wby_con *connection, void *userdata)
{
    printf("WebSocket closed\n");
    con = NULL;
}

static void
test_log(const char* text)
{
    printf("[debug] %s\n", text);
}

void *websocket_main(void *threadid)
{
    void *memory = NULL;
    wby_size needed_memory = 0;

    struct wby_config config;
    memset(&config, 0, sizeof config);
    config.userdata = NULL;
    config.address = "127.0.0.1";
    config.port = 8888;
    config.connection_max = 4;
    config.request_buffer_size = 2048;
    config.io_buffer_size = 8192;
    config.log = test_log;
    config.dispatch = dispatch;
    config.ws_connect = websocket_connect;
    config.ws_connected = websocket_connected;
    config.ws_frame = websocket_frame;
    config.ws_closed = websocket_closed;
    
    wby_init(&server, &config, &needed_memory);
    memory = calloc(needed_memory, 1);
    wby_start(&server, memory);

    printf("Awaiting WebSocket connection from Chrome extension.\n");
    for (;;) {
        send_req_if_any();

        wby_update(&server);
    }

    wby_stop(&server);
    free(memory);
#if defined(_WIN32)
    WSACleanup();
#endif
    return 0;
}

int
main(int argc, char **argv)
{
    pthread_t websocket_thread;
    pthread_create(&websocket_thread, NULL, websocket_main, NULL);
    return fuse_main(argc, argv, &hello_filesystem_operations, NULL);

}