#include "lsp_handler.h"
#include "../libs/cjson/cJSON.h"
#include "constants.h"
#include "helper_functions.h"
#include "node.h"
#include <ctype.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>

int lsp_stdin_fd = -1;
int lsp_stdout_fd = -1;
int lsp_pid = -1;
int version = 1;
int c_id = 1;
pthread_mutex_t lsp_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef enum {
  HOVER,
} LspRequestType;
typedef struct LspQueue LspQueue;
typedef struct LspQueue {
  LspRequestType type;
  int id;
  Node *buffer;
  struct LspQueue *next;
} LspQueue;

LspQueue *lsp_head = NULL;

void lsp_enqueue(LspRequestType type, int id, Node *buffer) {
  LspQueue *node = malloc(sizeof(LspQueue));
  node->type = type;
  node->id = id;
  node->buffer = buffer;
  node->next = NULL;
  pthread_mutex_lock(&lsp_mutex);
  if (!lsp_head) {
    lsp_head = node;
  } else {
    LspQueue *cur = lsp_head;
    while (cur->next)
      cur = cur->next;
    cur->next = node;
  }
  pthread_mutex_unlock(&lsp_mutex);
}

void measure_text(const char *text, int *out_lines, int *out_longest) {
  if (!text) {
    *out_lines = 0;
    *out_longest = 0;
    return;
  }
  char *copy = strdup(text);
  if (!copy) {
    *out_lines = 0;
    *out_longest = 0;
    return;
  }
  int lines = 0;
  int longest = 0;
  char *line = strtok(copy, "\n");
  while (line) {
    int len = strlen(line);
    if (len > longest)
      longest = len;
    lines++;
    line = strtok(NULL, "\n");
  }
  free(copy);
  *out_lines = lines + 1;
  *out_longest = longest;
}

void wrap_text(char *text, int max_width) {
  if (!text || max_width <= 0)
    return;
  int line_length = 0;
  char *read_pos = text;
  char *write_pos = text;
  char *last_space = NULL;
  while (*read_pos) {
    *write_pos = *read_pos;
    if (isspace(*read_pos)) {
      if (*read_pos != '\n')
        last_space = write_pos;
      if (*read_pos == '\n') {
        line_length = 0;
        last_space = NULL;
      } else {
        line_length++;
      }
    } else {
      line_length++;
    }
    if (line_length > max_width) {
      if (last_space != NULL) {
        *last_space = '\n';
        line_length = write_pos - last_space;
        last_space = NULL;
      } else {
        memmove(write_pos + 1, write_pos, strlen(write_pos) + 1);
        *write_pos = '\n';
        write_pos++;
        line_length = 1;
      }
    }
    read_pos++;
    write_pos++;
  }
}

void send_notification(cJSON *msg);

void handle_hover_response(Node *buffer, cJSON *msg) {
  cJSON *result = cJSON_GetObjectItem(msg, "result");
  cJSON *contents = cJSON_GetObjectItem(result, "contents");
  cJSON *text_json = cJSON_GetObjectItem(contents, "value");
  if (!text_json || text_json->type != cJSON_String)
    return;
  char *text = text_json->valuestring;
  wrap_text(text, 100);
  int lines, longest;
  measure_text(text, &lines, &longest);
  if (lines > 14)
    lines = 14;
  int x = buffer->Buffer.data->cursor_col + buffer->Buffer.calc_x;
  int y = buffer->Buffer.data->cursor_row + buffer->Buffer.calc_y;
  int width = longest + 1;
  int height = lines;
  if (x + width >= cols)
    x = cols - width - 1;
  if (y + height >= rows)
    y = rows - height - 1;
  char *f_text = strdup(text);
  Node *bubble = create_bubble(y, x, width, height, f_text);
  free(f_text);
  draw_bubble(bubble);
  for (int i = 0; i < bubble->Bubble.line_count; i++) {
    for (int j = 0; j < bubble->Bubble.lines[i].cluster_count; j++) {
      free(bubble->Bubble.lines[i].clusters[j].utf8);
    }
    free(bubble->Bubble.lines[i].clusters);
    free(bubble->Bubble.lines[i].highlights);
  }
  free(bubble->Bubble.lines);
  free(bubble);
}

void *lsp_thread(void *arg) {
  UNUSED(arg);
  while (1) {
    cJSON *msg = lsp_recv();
    if (!msg)
      continue;
    cJSON *id_json = cJSON_GetObjectItem(msg, "id");
    if (id_json) {
      int resp_id = id_json->valueint;
      pthread_mutex_lock(&lsp_mutex);
      LspQueue **cur = &lsp_head;
      while (*cur) {
        if ((*cur)->id == resp_id) {
          LspQueue *matched = *cur;
          *cur = matched->next;
          pthread_mutex_unlock(&lsp_mutex);
          if (matched->type == HOVER)
            handle_hover_response(matched->buffer, msg);
          free(matched);
          break;
        }
        cur = &(*cur)->next;
      }
      pthread_mutex_unlock(&lsp_mutex);
    } else {
      cJSON *method_json = cJSON_GetObjectItem(msg, "method");
      if (method_json && cJSON_IsString(method_json)) {
        const char *method = method_json->valuestring;
        // send_notification(msg);  TODO
      }
    }
    cJSON_Delete(msg);
  }
}

void send_notification(cJSON *msg) {
  char *dump = cJSON_Print(msg);
  wrap_text(dump, 200);
  Node *bubble = create_bubble(0, 0, 200, 30, dump);
  draw_bubble(bubble);
}

void lsp_setup(Node *buffer, uint8_t lang) {
  if (lang == RUBY) {
    char *argv[] = {"ruby-lsp", NULL};
    start_lsp("ruby-lsp", argv);
  } else if (lang == C_LANG) {
    char *argv[] = {"clangd", NULL};
    start_lsp("clangd", argv);
  } else {
    return;
  }
  cJSON *init = make_folder_init_request(1, buffer->Buffer.folder);
  lsp_send(init);
  cJSON_Delete(init);
  char *text = lines_to_string(buffer->Buffer.data);
  cJSON *req = make_open_file_request(buffer->Buffer.filename, "c", text, 1);
  lsp_send(req);
  cJSON_Delete(req);
  free(text);
  pthread_t lsp_recive_thread_id;
  if (pthread_create(&lsp_recive_thread_id, NULL, lsp_thread, NULL) != 0)
    die("pthread_create failed for lsp thread");
}

void lsp_change(Node *buffer, size_t start_line, size_t start_col,
                size_t end_line, size_t end_col, const char *new_text) {
  cJSON *req =
      make_change_file_request(buffer->Buffer.filename, version++, start_line,
                               start_col, end_line, end_col, new_text);
  lsp_send(req);
  cJSON_Delete(req);
}

void lsp_hover(Node *buffer, size_t line, size_t character) {
  cJSON *req =
      make_hover_request(++c_id, buffer->Buffer.filename, line, character);
  lsp_send(req);
  lsp_enqueue(HOVER, c_id, buffer);
  cJSON_Delete(req);
}

void handle_sigchld(int sig) {
  int status;
  pid_t pid;
  while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
    if (pid == lsp_pid)
      die("clangd exited");
  }
}

void start_lsp(const char *cmd, char *const argv[]) {
  int in_pipe[2];
  int out_pipe[2];
  if (pipe(in_pipe) == -1 || pipe(out_pipe) == -1) {
    perror("pipe");
    exit(1);
  }
  pid_t pid = fork();
  if (pid == -1) {
    perror("fork");
    exit(1);
  }
  if (pid == 0) {
    dup2(in_pipe[0], STDIN_FILENO);
    dup2(out_pipe[1], STDOUT_FILENO);
    int devnull = open("/dev/null", O_WRONLY);
    if (devnull != -1) {
      dup2(devnull, STDERR_FILENO);
      close(devnull);
    }
    close(in_pipe[1]);
    close(out_pipe[0]);
    execvp(cmd, argv);
    perror("execvp");
    _exit(1);
  }
  lsp_pid = pid;
  close(in_pipe[0]);
  close(out_pipe[1]);
  signal(SIGCHLD, handle_sigchld);
  lsp_stdin_fd = in_pipe[1];
  lsp_stdout_fd = out_pipe[0];
}

void lsp_send(cJSON *msg) {
  char *dump = cJSON_PrintUnformatted(msg);
  int len = strlen(dump);
  dprintf(lsp_stdin_fd, "Content-Length: %d\r\n\r\n%s", len, dump);
  free(dump);
}

cJSON *lsp_recv() {
  char header[256];
  int content_length = 0;
  char c;
  int pos = 0;
  int newlines = 0;
  while (read(lsp_stdout_fd, &c, 1) == 1) {
    header[pos++] = c;
    if (pos >= sizeof(header) - 1)
      break;
    if (c == '\n') {
      header[pos] = '\0';
      if (strncmp(header, "Content-Length:", 15) == 0)
        content_length = atoi(header + 15);
      if (strcmp(header, "\r\n") == 0)
        break;
      pos = 0;
    }
  }
  if (content_length <= 0)
    return NULL;
  char *buf = malloc(content_length + 1);
  if (!buf)
    return NULL;
  int got = 0;
  while (got < content_length) {
    int n = read(lsp_stdout_fd, buf + got, content_length - got);
    if (n <= 0)
      break;
    got += n;
  }
  buf[got] = '\0';
  cJSON *msg = cJSON_Parse(buf);
  free(buf);
  return msg;
}

cJSON *make_hover_request(int id, const char *uri, size_t line,
                          size_t character) {
  cJSON *root = cJSON_CreateObject();
  cJSON_AddStringToObject(root, "jsonrpc", "2.0");
  cJSON_AddNumberToObject(root, "id", id);
  cJSON_AddStringToObject(root, "method", "textDocument/hover");
  cJSON *params = cJSON_CreateObject();
  cJSON *doc = cJSON_CreateObject();
  char *f_uri = malloc(strlen(uri) + 9);
  sprintf(f_uri, "file:///%s", uri);
  cJSON_AddStringToObject(doc, "uri", f_uri);
  cJSON_AddItemToObject(params, "textDocument", doc);
  free(f_uri);
  cJSON *pos = cJSON_CreateObject();
  cJSON_AddNumberToObject(pos, "line", line);
  cJSON_AddNumberToObject(pos, "character", character);
  cJSON_AddItemToObject(params, "position", pos);
  cJSON_AddItemToObject(root, "params", params);
  return root;
}

cJSON *make_folder_init_request(int id, const char *root_uri) {
  cJSON *init = cJSON_CreateObject();
  cJSON_AddStringToObject(init, "jsonrpc", "2.0");
  cJSON_AddNumberToObject(init, "id", id);
  cJSON_AddStringToObject(init, "method", "initialize");
  cJSON *params = cJSON_CreateObject();
  char *uri = malloc(strlen(root_uri) + 9);
  sprintf(uri, "file:///%s", root_uri);
  cJSON_AddStringToObject(params, "rootUri", uri);
  free(uri);
  cJSON_AddItemToObject(init, "params", params);
  return init;
}

cJSON *make_open_file_request(const char *uri, const char *languageId,
                              const char *text, int version) {
  cJSON *msg = cJSON_CreateObject();
  cJSON_AddStringToObject(msg, "jsonrpc", "2.0");
  cJSON_AddStringToObject(msg, "method", "textDocument/didOpen");
  cJSON *params = cJSON_CreateObject();
  cJSON *doc = cJSON_CreateObject();
  char *f_uri = malloc(strlen(uri) + 9);
  sprintf(f_uri, "file:///%s", uri);
  cJSON_AddStringToObject(doc, "uri", f_uri);
  free(f_uri);
  cJSON_AddStringToObject(doc, "languageId", languageId);
  cJSON_AddNumberToObject(doc, "version", version);
  cJSON_AddStringToObject(doc, "text", text);
  cJSON_AddItemToObject(params, "textDocument", doc);
  cJSON_AddItemToObject(msg, "params", params);
  return msg;
}

cJSON *make_change_file_request(const char *uri, int version, size_t start_line,
                                size_t start_col, size_t end_line,
                                size_t end_col, const char *new_text) {
  cJSON *root = cJSON_CreateObject();
  cJSON_AddStringToObject(root, "jsonrpc", "2.0");
  cJSON_AddStringToObject(root, "method", "textDocument/didChange");
  cJSON *params = cJSON_CreateObject();
  cJSON *doc = cJSON_CreateObject();
  char *f_uri = malloc(strlen(uri) + 9);
  sprintf(f_uri, "file:///%s", uri);
  cJSON_AddStringToObject(doc, "uri", f_uri);
  free(f_uri);
  cJSON_AddNumberToObject(doc, "version", version);
  cJSON_AddItemToObject(params, "textDocument", doc);
  cJSON *changes = cJSON_CreateArray();
  cJSON *change = cJSON_CreateObject();
  cJSON *range = cJSON_CreateObject();
  cJSON *start = cJSON_CreateObject();
  cJSON_AddNumberToObject(start, "line", start_line);
  cJSON_AddNumberToObject(start, "character", start_col);
  cJSON *end = cJSON_CreateObject();
  cJSON_AddNumberToObject(end, "line", end_line);
  cJSON_AddNumberToObject(end, "character", end_col);
  cJSON_AddItemToObject(range, "start", start);
  cJSON_AddItemToObject(range, "end", end);
  cJSON_AddItemToObject(change, "range", range);
  cJSON_AddStringToObject(change, "text", new_text);
  cJSON_AddItemToArray(changes, change);
  cJSON_AddItemToObject(params, "contentChanges", changes);
  cJSON_AddItemToObject(root, "params", params);
  return root;
}
