// #include "constants.h"
#include "editor.h"
#include "../libs/tree-sitter/lib/include/tree_sitter/api.h"
#include "helper_functions.h"
#include "highlighters.h"
#include "node.h"
#include <grapheme.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

BufferData *init_buffer(void) {
  BufferData *e = calloc(1, sizeof(BufferData));
  if (!e)
    die("calloc failed");
  e->lines = calloc(1, sizeof(Line *));
  if (!e->lines)
    die("calloc failed");
  e->lines[0] = calloc(1, sizeof(Line));
  if (!e->lines[0])
    die("calloc failed");
  e->lines[0]->clusters = NULL;
  e->lines[0]->cluster_count = 0;
  e->lines[0]->highlights = NULL;
  e->lines[0]->highlight_count = 0;
  e->virtual_lines = calloc(1, sizeof(VirtLine *));
  if (!e->virtual_lines)
    die("calloc failed");
  e->virtual_lines[0] = calloc(1, sizeof(VirtLine));
  if (!e->virtual_lines[0])
    die("calloc failed");
  e->virtual_lines[0]->lines = NULL;
  e->virtual_lines[0]->line_count = 0;
  e->line_count = 1;
  e->parser = NULL;
  e->tree = NULL;
  e->fold_count = 0;
  e->folds = NULL;
  e->cursor_row = 0;
  e->cursor_col = 0;
  e->scroll_row = 0;
  e->scroll_col = 0;
  e->desired_col = 0;
  e->selecting = 0;
  e->selection_start_row = 0;
  e->selection_start_col = 0;
  return e;
}

void start_selection(Node *buffer) {
  if (!buffer || buffer->type != BUFFER)
    return;
  if (!buffer->Buffer.data->selecting) {
    buffer->Buffer.data->selecting = 1;
    buffer->Buffer.data->selection_start_row = buffer->Buffer.data->cursor_row;
    buffer->Buffer.data->selection_start_col = buffer->Buffer.data->cursor_col;
  }
}

void end_selection(Node *buffer) {
  if (!buffer || buffer->type != BUFFER)
    return;
  if (buffer->Buffer.data->selecting) {
    buffer->Buffer.data->selecting = 0;
    buffer->Buffer.data->selection_start_row = 0;
    buffer->Buffer.data->selection_start_col = 0;
  }
}

char *get_selection(Node *buffer) {
  if (!buffer || buffer->type != BUFFER)
    return NULL;
  BufferData *e = buffer->Buffer.data;
  if (!e || !e->selecting)
    return NULL;
  int start_row = e->selection_start_row;
  int start_col = e->selection_start_col;
  int end_row = e->cursor_row;
  int end_col = e->cursor_col;
  int space_needed = 0;
  if (start_row > end_row || (start_row == end_row && start_col > end_col)) {
    int tmp_row = start_row, tmp_col = start_col;
    start_row = end_row;
    start_col = end_col;
    end_row = tmp_row;
    end_col = tmp_col;
  }
  end_col += 1;
  for (int row = start_row; row <= end_row; row++) {
    Line *l = e->lines[row];
    if (!l)
      continue;
    int s = 0;
    int e_col = (int)l->cluster_count;
    if (row == start_row)
      s = start_col;
    if (row == end_row)
      e_col = end_col;
    if (s < 0)
      s = 0;
    if (e_col > (int)l->cluster_count)
      e_col = (int)l->cluster_count;
    if (s < e_col) {
      for (int c = s; c < e_col; c++) {
        space_needed += strlen(l->clusters[c].utf8);
      }
      space_needed++;
    }
  }
  char *selection = calloc(space_needed + 1, sizeof(char));
  if (!selection)
    die("calloc failed");
  int offset = 0;
  for (int row = start_row; row <= end_row; row++) {
    Line *l = e->lines[row];
    if (!l)
      continue;
    int s = 0;
    int e_col = (int)l->cluster_count;
    if (row == start_row)
      s = start_col;
    if (row == end_row)
      e_col = end_col;
    if (s < 0)
      s = 0;
    if (e_col > (int)l->cluster_count)
      e_col = (int)l->cluster_count;
    if (s < e_col) {
      for (int c = s; c < e_col; c++) {
        int len = strlen(l->clusters[c].utf8);
        memcpy(selection + offset, l->clusters[c].utf8, len);
        offset += len;
      }
      selection[offset++] = '\n';
    }
  }
  selection[offset] = '\0';
  return selection;
}

void scroll_buffer(Node *buffer, int dy, int dx) {
  BufferData *e = buffer->Buffer.data;
  if (!e || e->line_count == 0)
    return;
  int vis_total = visible_count(e);
  int max_vrow = vis_total - 1;
  int new_vscroll = e->scroll_row + dy;
  if (new_vscroll < 0)
    new_vscroll = 0;
  else if (new_vscroll > max_vrow)
    new_vscroll = max_vrow;
  e->scroll_row = new_vscroll;
  int vcursor = physical_to_visible(e, e->cursor_row);
  if ((size_t)vcursor < e->scroll_row)
    vcursor = e->scroll_row;
  else if ((size_t)vcursor >= e->scroll_row + buffer->Buffer.calc_rows)
    vcursor = e->scroll_row + buffer->Buffer.calc_rows - 1;
  e->cursor_row = visible_to_physical(e, vcursor);
  int max_width = 0;
  if ((size_t)e->cursor_row < e->line_count)
    max_width = (int)e->lines[e->cursor_row]->cluster_count;
  int new_scroll_col = e->scroll_col + dx;
  if (new_scroll_col < 0)
    new_scroll_col = 0;
  else if (new_scroll_col > max_width)
    new_scroll_col = max_width;
  e->scroll_col = new_scroll_col;
  if (e->cursor_col < e->scroll_col)
    e->cursor_col = e->scroll_col;
  else if (e->cursor_col >= e->scroll_col + buffer->Buffer.calc_cols) {
    int cursor_col = e->scroll_col + buffer->Buffer.calc_cols - 1;
    if (cursor_col <= max_width)
      e->cursor_col = cursor_col;
    else
      e->cursor_col = max_width;
  }
  move_cursor(buffer, 0, 0);
}

void set_cursor(Node *buffer, int row, int col) {
  BufferData *e = buffer->Buffer.data;
  if (!e || !buffer || buffer->type != BUFFER)
    return;
  if (row < 0)
    row = 0;
  else if ((size_t)row >= e->line_count)
    row = (int)e->line_count - 1;
  Line *line = e->lines[row];
  if (col < 0)
    col = 0;
  else if ((size_t)col > line->cluster_count)
    col = line->cluster_count;
  e->cursor_row = row;
  e->cursor_col = col;
  e->desired_col = col;
  int scr_top = e->scroll_row;
  int scr_bottom = e->scroll_row + buffer->Buffer.calc_rows - 1;
  if (row < scr_top)
    e->scroll_row = row;
  else if (row > scr_bottom)
    e->scroll_row = row - buffer->Buffer.calc_rows + 1;
  int scr_left = e->scroll_col;
  int scr_right = e->scroll_col + buffer->Buffer.calc_cols - 1;
  if (col < scr_left)
    e->scroll_col = col;
  else if (col > scr_right)
    e->scroll_col = col - buffer->Buffer.calc_cols + 1;
}

void move_cursor(Node *buffer, int dx, int dy) {
  if (!buffer || buffer->type != BUFFER)
    return;
  BufferData *e = buffer->Buffer.data;
  if (!e || e->line_count == 0)
    return;
  int new_row = (int)e->cursor_row + dy;
  new_row = clamp_to_visible(e, new_row, (dy > 0 ? 1 : -1));
  Line *line = e->lines[new_row];
  int new_col = e->cursor_col + dx;
  if (dx != 0) {
    if (new_col < 0)
      new_col = 0;
    else if ((size_t)new_col > line->cluster_count)
      new_col = line->cluster_count;
    e->desired_col = new_col;
  } else {
    new_col = e->desired_col;
    if ((size_t)new_col > line->cluster_count)
      new_col = line->cluster_count;
  }
  e->cursor_row = new_row;
  e->cursor_col = new_col;
  int scr_top = e->scroll_row;
  int scr_bottom = e->scroll_row + buffer->Buffer.calc_rows - 1;
  int visible_row = 0, vcount = 0;
  for (size_t i = 0; i <= (size_t)new_row; i++)
    if (!line_is_hidden(e, i))
      visible_row = vcount++;
  if (visible_row < scr_top)
    e->scroll_row = visible_row;
  else if (visible_row > scr_bottom)
    e->scroll_row = visible_row - buffer->Buffer.calc_rows + 1;
  int scr_left = e->scroll_col;
  int scr_right = e->scroll_col + buffer->Buffer.calc_cols - 1;
  if (new_col < scr_left)
    e->scroll_col = new_col;
  else if (new_col > scr_right)
    e->scroll_col = new_col - buffer->Buffer.calc_cols + 1;
}

void move_cursor_to_mouse(Node *buffer, int mouse_x, int mouse_y) {
  if (!buffer || buffer->type != BUFFER)
    return;
  BufferData *e = buffer->Buffer.data;
  if (!e || e->line_count == 0)
    return;
  int visible_row = e->scroll_row + mouse_y;
  size_t physical_row = visible_to_physical(e, visible_row);
  Line *line = e->lines[physical_row];
  int target_col = 0;
  int screen_x = 0;
  for (size_t i = 0; i < line->cluster_count; i++) {
    int w = line->clusters[i].width;
    if (screen_x + w / 2 >= mouse_x)
      break;
    screen_x += w;
    target_col++;
  }
  target_col += e->scroll_col;
  if ((size_t)target_col > line->cluster_count)
    target_col = line->cluster_count;
  e->cursor_row = physical_row;
  e->cursor_col = target_col;
  e->desired_col = target_col;
  if ((size_t)visible_row < e->scroll_row)
    e->scroll_row = visible_row;
  else if ((size_t)visible_row >= e->scroll_row + buffer->Buffer.calc_rows)
    e->scroll_row = visible_row - buffer->Buffer.calc_rows + 1;
  if (e->cursor_col < e->scroll_col)
    e->scroll_col = e->cursor_col;
  else if (e->cursor_col >= e->scroll_col + buffer->Buffer.calc_cols)
    e->scroll_col = e->cursor_col - buffer->Buffer.calc_cols + 1;
}

void destroy_virt(BufferData *e, size_t idx) {
  if (!e || idx >= e->line_count)
    return;
  VirtLine *virt = e->virtual_lines[idx];
  if (!virt)
    return;
  for (size_t i = 0; i < virt->line_count; i++) {
    Line *l = virt->lines[i];
    if (!l)
      continue;
    for (size_t c = 0; c < l->cluster_count; c++)
      free(l->clusters[c].utf8);
    free(l->clusters);
    free(l->highlights);
    free(l);
  }
  free(virt->lines);
  free(virt);
  for (size_t i = idx; i < e->line_count - 1; i++) {
    e->virtual_lines[i] = e->virtual_lines[i + 1];
  }
  e->virtual_lines =
      realloc(e->virtual_lines, e->line_count * sizeof(VirtLine *));
}

Coords delete_text(Node *buffer, ssize_t count, int cx, int cy) {
  if (!buffer || buffer->type != BUFFER)
    return (Coords){0, 0};
  BufferData *e = buffer->Buffer.data;
  if (!e || e->line_count == 0 || count == 0)
    return (Coords){0, 0};
  TSInputEdit edit = {};
  bool backward = count > 0;
  int removed = 0;
  int removed_col = 0;
  int removed_rows = 0;
  int tmp_byte = grapheme_to_byte_col(e->lines[cx], cy);
  if (!backward) {
    edit.start_byte = 0;
    for (int i = 0; i < cx; i++)
      edit.start_byte += grapheme_to_byte(e->lines[i]) + 1;
    edit.start_byte += tmp_byte;
    edit.new_end_byte = edit.start_byte;
    edit.start_point.row = cx;
    edit.start_point.column = tmp_byte;
    edit.new_end_point = edit.start_point;
  } else {
    edit.old_end_byte = 0;
    for (int i = 0; i < cx; i++)
      edit.old_end_byte += grapheme_to_byte(e->lines[i]) + 1;
    edit.old_end_byte += tmp_byte;
    edit.old_end_point.row = cx;
    edit.old_end_point.column = tmp_byte;
  }
  ssize_t remaining = backward ? count : -count;
  int row = cx;
  int col = cy;
  int f_col = cy;
  while (remaining != 0) {
    Line *line = e->lines[row];
    if (backward) {
      if (col == 0) {
        if (row == 0)
          break;
        Line *prev = e->lines[row - 1];
        f_col = prev->cluster_count;
        append_line(prev, line);
        free(line->clusters);
        if (line->highlights)
          free(line->highlights);
        free(line);
        for (size_t i = row; i < e->line_count - 1; i++)
          e->lines[i] = e->lines[i + 1];
        destroy_virt(e, row);
        e->line_count--;
        row--;
        col = prev->cluster_count;
      } else {
        size_t del_idx = col - 1;
        free(line->clusters[del_idx].utf8);
        for (size_t i = del_idx; i < line->cluster_count - 1; i++)
          line->clusters[i] = line->clusters[i + 1];
        line->cluster_count--;
        line->clusters =
            realloc(line->clusters, line->cluster_count * sizeof(Cluster));
        col--;
        f_col = col;
      }
      remaining--;
    } else {
      if ((size_t)col >= line->cluster_count) {
        if ((size_t)(row + 1) >= e->line_count)
          break;
        Line *next = e->lines[row + 1];
        f_col = line->cluster_count;
        append_line(line, next);
        free(next->clusters);
        if (next->highlights)
          free(next->highlights);
        free(next);
        for (size_t i = row + 1; i < e->line_count - 1; i++)
          e->lines[i] = e->lines[i + 1];
        destroy_virt(e, row + 1);
        e->line_count--;
        removed++;
        removed_rows++;
        removed_col = 0;
      } else {
        removed += strlen(line->clusters[col].utf8);
        removed_col += strlen(line->clusters[col].utf8);
        free(line->clusters[col].utf8);
        f_col = col;
        for (size_t i = col; i < line->cluster_count - 1; i++)
          line->clusters[i] = line->clusters[i + 1];
        line->cluster_count--;
        line->clusters =
            realloc(line->clusters, line->cluster_count * sizeof(Cluster));
      }
      remaining--;
    }
  }
  Coords coords = (Coords){row, f_col};
  if (!backward) {
    edit.old_end_byte = edit.start_byte + removed;
    edit.old_end_point = edit.start_point;
    edit.old_end_point.row += removed_rows;
    edit.old_end_point.column += removed_col;
  } else {
    edit.start_byte = 0;
    for (int i = 0; i < row; i++)
      edit.start_byte += grapheme_to_byte(e->lines[i]) + 1;
    tmp_byte = grapheme_to_byte_col(e->lines[row], f_col);
    edit.start_byte += tmp_byte;
    edit.new_end_byte = edit.start_byte;
    edit.start_point.row = row;
    edit.start_point.column = tmp_byte;
    edit.new_end_point = edit.start_point;
  }
  if (e->tree)
    ts_tree_edit(e->tree, &edit);
  if (e->lsp_change_func)
    e->lsp_change_func(buffer, edit.start_point.row, edit.start_point.column,
                       edit.old_end_point.row, edit.old_end_point.column, "");
  return coords;
}

Coords insert_text(Node *buffer, const char *text, int cx, int cy) {
  if (!buffer || buffer->type != BUFFER)
    return (Coords){0, 0};
  BufferData *e = buffer->Buffer.data;
  if (!e || !text)
    return (Coords){0, 0};
  TSInputEdit edit = {};
  edit.start_byte = 0;
  for (int i = 0; i < cx; i++)
    edit.start_byte += grapheme_to_byte(e->lines[i]) + 1;
  int tmp_byte = grapheme_to_byte_col(e->lines[cx], cy);
  edit.start_byte += tmp_byte;
  edit.old_end_byte = edit.start_byte;
  edit.start_point.row = e->cursor_row;
  edit.start_point.column = tmp_byte;
  edit.old_end_point = edit.start_point;
  int c_row = cx;
  int c_col = cy;
  Line *line = e->lines[c_row];
  Line tail = take_last_clusters(line, line->cluster_count - c_col);
  const char *start = text;
  const char *current = text;
  bool first_line = true;
  int lines_added = 0;
  while (*current != '\0' || current > start) {
    const char *line_end = current;
    while (*line_end != '\0' && *line_end != '\n')
      line_end++;
    size_t line_length = line_end - current;
    char *line_content = malloc(line_length + 1);
    if (!line_content)
      die("malloc failed");
    if (line_length > 0)
      memcpy(line_content, current, line_length);
    line_content[line_length] = '\0';
    Line new_line = split_graphemes(line_content);
    free(line_content);
    if (first_line) {
      append_line(line, &new_line);
      first_line = false;
    } else {
      Line *tmp_line = calloc(1, sizeof(Line));
      if (!tmp_line)
        die("calloc failed");
      *tmp_line = new_line;
      insert_line(e, c_row + lines_added + 1, tmp_line);
      lines_added++;
    }
    if (*line_end == '\n')
      current = line_end + 1;
    else
      break;
  }
  Coords cursor = {};
  if (lines_added > 0) {
    cursor.col = e->lines[c_row + lines_added]->cluster_count;
    append_line(e->lines[c_row + lines_added], &tail);
    cursor.row = c_row + lines_added;
  } else {
    int col_offset = line->cluster_count;
    append_line(line, &tail);
    cursor.col = col_offset;
    cursor.row = c_row;
  }
  for (size_t i = 0; i < cursor.row; i++)
    edit.new_end_byte += grapheme_to_byte(e->lines[i]) + 1;
  tmp_byte = grapheme_to_byte_col(e->lines[cursor.row], cursor.col);
  edit.new_end_byte += tmp_byte;
  edit.new_end_point.row = e->cursor_row;
  edit.new_end_point.column = tmp_byte;
  if (e->tree)
    ts_tree_edit(e->tree, &edit);
  if (e->lsp_change_func)
    e->lsp_change_func(buffer, edit.start_point.row, edit.start_point.column,
                       edit.old_end_point.row, edit.old_end_point.column, text);
  return cursor;
}

Coords replace_text(Node *buffer, const char *text, int sx, int sy, int ex,
                    int ey) {
  if (!buffer || buffer->type != BUFFER)
    return (Coords){0, 0};
  BufferData *e = buffer->Buffer.data;
  if (!e || !text)
    return (Coords){0, 0};
  ssize_t count = 0;
  if (sx == ex) {
    count = ey - sy;
  } else {
    count = e->lines[sx]->cluster_count - sy;
    for (int r = sx + 1; r < ex; r++)
      count += e->lines[r]->cluster_count + 1;
    count += ey + 1;
  }
  Coords coords = delete_text(buffer, -count, sx, sy);
  return insert_text(buffer, text, coords.row, coords.col);
}

Line take_last_clusters(Line *line, size_t n) {
  Line temp = {0};
  if (!line || n == 0 || line->cluster_count == 0)
    return temp;
  if (n > line->cluster_count)
    n = line->cluster_count;
  temp.cluster_count = n;
  temp.clusters = malloc(n * sizeof(Cluster));
  if (!temp.clusters)
    die("malloc failed");
  for (size_t i = 0; i < n; i++) {
    temp.clusters[i] = line->clusters[line->cluster_count - n + i];
    line->clusters[line->cluster_count - n + i].utf8 = NULL;
  }
  line->cluster_count -= n;
  line->clusters =
      realloc(line->clusters, line->cluster_count * sizeof(Cluster));
  return temp;
}

void append_line(Line *dest, Line *src) {
  if (!dest || !src || src->cluster_count == 0)
    return;
  size_t new_count = dest->cluster_count + src->cluster_count;
  Cluster *new_clusters = realloc(dest->clusters, new_count * sizeof(Cluster));
  if (!new_clusters)
    die("realloc failed");
  dest->clusters = new_clusters;
  for (size_t i = 0; i < src->cluster_count; i++) {
    dest->clusters[dest->cluster_count + i] = src->clusters[i];
    src->clusters[i].utf8 = NULL;
  }
  dest->cluster_count = new_count;
  free(src->clusters);
  src->clusters = NULL;
  src->cluster_count = 0;
}

void insert_line(BufferData *e, size_t index, Line *new_line) {
  if (!e || index > e->line_count)
    return;
  Line **new_lines = realloc(e->lines, (e->line_count + 1) * sizeof(Line *));
  if (!new_lines)
    die("realloc failed");
  e->lines = new_lines;
  for (size_t i = e->line_count; i > index; i--)
    e->lines[i] = e->lines[i - 1];
  e->lines[index] = new_line;
  e->lines[index]->highlight_count = 0;
  e->lines[index]->highlights = NULL;
  VirtLine **new_virtual_lines =
      realloc(e->virtual_lines, (e->line_count + 1) * sizeof(VirtLine *));
  if (!new_virtual_lines)
    die("realloc failed");
  e->virtual_lines = new_virtual_lines;
  for (size_t i = e->line_count; i > index; i--)
    e->virtual_lines[i] = e->virtual_lines[i - 1];
  VirtLine *new_virtual_line = calloc(1, sizeof(VirtLine));
  if (!new_virtual_line)
    die("calloc failed");
  new_virtual_line->lines = NULL;
  new_virtual_line->line_count = 0;
  e->virtual_lines[index] = new_virtual_line;
  e->line_count++;
}

Line *clone_line(Line *src) {
  if (!src)
    return NULL;
  Line *copy = malloc(sizeof(Line));
  if (!copy)
    die("malloc failed");
  *copy = *src;
  if (src->cluster_count > 0) {
    copy->clusters = malloc(src->cluster_count * sizeof(Cluster));
    if (!copy->clusters)
      die("malloc failed");
    for (size_t i = 0; i < src->cluster_count; i++) {
      copy->clusters[i] = src->clusters[i];
      if (src->clusters[i].utf8) {
        copy->clusters[i].utf8 = strdup(src->clusters[i].utf8);
        if (!copy->clusters[i].utf8)
          die("strdup failed");
      }
    }
  } else {
    copy->clusters = NULL;
  }
  if (src->highlight_count > 0) {
    copy->highlights = malloc(src->highlight_count * sizeof(Highlight));
    if (!copy->highlights)
      die("malloc failed");
    for (size_t i = 0; i < src->highlight_count; i++)
      copy->highlights[i] = src->highlights[i];
    copy->highlight_count = src->highlight_count;
  } else {
    copy->highlights = NULL;
    copy->highlight_count = 0;
  }
  return copy;
}
