#include "search.h"

#include <regex.h>
#include <string.h>

#include <wayland-client.h>
#include <xkbcommon/xkbcommon-compose.h>

#define LOG_MODULE "search"
#define LOG_ENABLE_DBG 0
#include "char32.h"
#include "commands.h"
#include "config.h"
#include "extract.h"
#include "grid.h"
#include "input.h"
#include "key-binding.h"
#include "log.h"
#include "misc.h"
#include "quirks.h"
#include "render.h"
#include "selection.h"
#include "session.h"
#include "shm.h"
#include "unicode-mode.h"
#include "util.h"
#include "xmalloc.h"

/* Hard cap on counting matches, to keep the counter cheap on huge
 * scrollbacks with very generic queries. */
#define SEARCH_COUNT_CAP 9999

/*
 * Ensures a "new" viewport doesn't contain any unallocated rows.
 *
 * This is done by first checking if the *first* row is NULL. If so,
 * we move the viewport *forward*, until the first row is non-NULL. At
 * this point, the entire viewport should be allocated rows only.
 *
 * If the first row already was non-NULL, we instead check the *last*
 * row, and if it is NULL, we move the viewport *backward* until the
 * last row is non-NULL.
 */
static int ensure_view_is_allocated(struct terminal *term, int new_view) {
  struct grid *grid = term->grid;
  int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1);

  if (grid->rows[new_view] == NULL) {
    while (grid->rows[new_view] == NULL)
      new_view = (new_view + 1) & (grid->num_rows - 1);
  }

  else if (grid->rows[view_end] == NULL) {
    while (grid->rows[view_end] == NULL) {
      new_view--;
      if (new_view < 0)
        new_view += grid->num_rows;
      view_end = (new_view + term->rows - 1) & (grid->num_rows - 1);
    }
  }

#if defined(_DEBUG)
  for (size_t r = 0; r < term->rows; r++)
    xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL);
#endif

  return new_view;
}

static bool search_ensure_size(struct terminal *term, size_t wanted_size) {
  while (wanted_size >= term->search.sz) {
    size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2;
    char32_t *new_buf =
        realloc(term->search.buf, new_sz * sizeof(term->search.buf[0]));

    if (new_buf == NULL) {
      LOG_ERRNO("failed to resize search buffer");
      return false;
    }

    term->search.buf = new_buf;
    term->search.sz = new_sz;
  }

  return true;
}

static bool has_wrapped_around_left(const struct terminal *term,
                                    int abs_row_no) {
  int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no);
  return rebased_row == term->grid->num_rows - 1 ||
         term->grid->rows[abs_row_no] == NULL;
}

static bool has_wrapped_around_right(const struct terminal *term,
                                     int abs_row_no) {
  int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no);
  return rebased_row == 0;
}

static void search_history_push(struct terminal *term, const char32_t *buf,
                                size_t len) {
  if (len == 0)
    return;

  /* Avoid pushing a duplicate of the most recent entry */
  if (term->search.history_tail != NULL &&
      term->search.history_tail->len == len &&
      c32ncmp(term->search.history_tail->buf, buf, len) == 0)
    return;

  struct search_history_entry *e = xcalloc(1, sizeof(*e));
  e->buf = xmalloc((len + 1) * sizeof(char32_t));
  memcpy(e->buf, buf, len * sizeof(char32_t));
  e->buf[len] = U'\0';
  e->len = len;

  e->prev = term->search.history_tail;
  if (term->search.history_tail != NULL)
    term->search.history_tail->next = e;
  else
    term->search.history_head = e;
  term->search.history_tail = e;
}

static void search_cancel_keep_selection(struct terminal *term) {
  struct wl_window *win = term->window;
  wayl_win_subsurface_destroy(&win->search);

  if (term->search.len > 0) {
    /* Save into history */
    search_history_push(term, term->search.buf, term->search.len);

    free(term->search.last.buf);
    term->search.last.buf = term->search.buf;
    term->search.last.len = term->search.len;
  } else
    free(term->search.buf);

  /* Free compiled regex */
  if (term->search.regex_compiled != NULL) {
    regfree(term->search.regex_compiled);
    free(term->search.regex_compiled);
    term->search.regex_compiled = NULL;
  }
  term->search.regex_valid = false;

  term->search.buf = NULL;
  term->search.len = term->search.sz = 0;

  term->search.cursor = 0;
  term->search.match = (struct coord){-1, -1};
  term->search.match_len = 0;
  term->search.total_count = 0;
  term->search.current_idx = 0;
  term->search.wrapped = false;
  term->search.history_pos = NULL;
  term->is_searching = false;
  if (term->search.mode == SEARCH_MODE_SESSION_LOAD)
    session_picker_free(term);
  term->search.mode = SEARCH_MODE_NORMAL;
  term->render.search_glyph_offset = 0;

  /* Reset IME state */
  if (term_ime_is_enabled(term)) {
    term_ime_disable(term);
    term_ime_enable(term);
  }

  term_xcursor_update(term);
  render_refresh(term);
}

void search_begin(struct terminal *term) {
  LOG_DBG("search: begin");

  search_cancel_keep_selection(term);
  selection_cancel(term);

  /* Reset IME state */
  if (term_ime_is_enabled(term)) {
    term_ime_disable(term);
    term_ime_enable(term);
  }

  /* On-demand instantiate wayland surface */
  bool ret =
      wayl_win_subsurface_new(term->window, &term->window->search, false);
  xassert(ret);

  const struct grid *grid = term->grid;
  term->search.original_view = grid->view;
  term->search.view_followed_offset = grid->view == grid->offset;
  term->is_searching = true;

  term->search.len = 0;
  term->search.sz = 64;
  term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0]));
  term->search.buf[0] = U'\0';
  term->search.mode = SEARCH_MODE_NORMAL;

  term_xcursor_update(term);
  render_refresh_search(term);
}

void search_begin_session(struct terminal *term, enum search_mode mode) {
  search_begin(term);
  term->search.mode = mode;
  if (mode == SEARCH_MODE_SESSION_LOAD)
    session_picker_init(term);
  render_refresh_search(term);
}

void search_cancel(struct terminal *term) {
  if (!term->is_searching)
    return;

  search_cancel_keep_selection(term);
  selection_cancel(term);
}

void search_selection_cancelled(struct terminal *term) {
  term->search.match = (struct coord){-1, -1};
  term->search.match_len = 0;
  render_refresh_search(term);
}

static void search_update_selection(struct terminal *term,
                                    const struct range *match) {
  struct grid *grid = term->grid;
  int start_row = match->start.row;
  int start_col = match->start.col;
  int end_row = match->end.row;
  int end_col = match->end.col;

  xassert(start_row >= 0);
  xassert(start_row < grid->num_rows);

  bool move_viewport = true;

  int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1);
  if (view_end >= grid->view) {
    /* Viewport does *not* wrap around */
    if (start_row >= grid->view && end_row <= view_end)
      move_viewport = false;
  } else {
    /* Viewport wraps */
    if (start_row >= grid->view || end_row <= view_end)
      move_viewport = false;
  }

  if (move_viewport) {
    int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row);

    rebased_new_view -= term->rows / 2;
    rebased_new_view =
        min(max(rebased_new_view, 0), grid->num_rows - term->rows);

    const int old_view = grid->view;
    int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view);

    /* Scrollback may not be completely filled yet */
    {
      const int mask = grid->num_rows - 1;
      while (grid->rows[new_view] == NULL)
        new_view = (new_view + 1) & mask;
    }

#if defined(_DEBUG)
    /* Verify all to-be-visible rows have been allocated */
    for (int r = 0; r < term->rows; r++)
      xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL);
#endif

#if defined(_DEBUG)
    {
      int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row);
      int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view);
      xassert(rel_view <= rel_start_row);
      xassert(rel_start_row < rel_view + term->rows);
    }
#endif

    /* Update view */
    grid->view = new_view;
    if (new_view != old_view)
      term_damage_view(term);
  }

  if (start_row != term->search.match.row ||
      start_col != term->search.match.col ||

      /* Pointer leave events trigger selection_finalize() :/ */
      !term->selection.ongoing) {
    int selection_row = start_row - grid->view + grid->num_rows;
    selection_row &= grid->num_rows - 1;

    selection_start(term, start_col, selection_row, SELECTION_CHAR_WISE, false);

    term->search.match.row = start_row;
    term->search.match.col = start_col;
  }

  /* Update selection endpoint */
  {
    int selection_row = end_row - grid->view + grid->num_rows;
    selection_row &= grid->num_rows - 1;
    selection_update(term, end_col, selection_row);
  }
}

static bool search_is_case_sensitive(const struct terminal *term) {
  switch (term->search.case_mode) {
  case SEARCH_CASE_SENSITIVE:
    return true;
  case SEARCH_CASE_INSENSITIVE:
    return false;
  case SEARCH_CASE_SMART:
    return hasc32upper(term->search.buf);
  }
  return true;
}

static bool search_cell_is_word(const struct terminal *term,
                                const struct cell *cell) {
  char32_t base = cell->wc;
  if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) {
    const struct composed *c =
        composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
    base = c->chars[0];
  }
  if (base == 0 || base >= CELL_SPACER)
    return false;
  return isword(base, false, term->conf->word_delimiters);
}

/* True if the cell *immediately before* (col-1, row), wrapping back a
 * row if needed, is a word character. Returns false at scrollback
 * boundaries (treat boundary as non-word). */
static bool search_neighbor_is_word(const struct terminal *term, int row,
                                    int col, bool look_right) {
  const struct grid *grid = term->grid;
  int r = row, c = col;

  if (look_right) {
    c++;
    if (c >= term->cols) {
      r = (r + 1) & (grid->num_rows - 1);
      c = 0;
      if (has_wrapped_around_right(term, r))
        return false;
    }
  } else {
    c--;
    if (c < 0) {
      r = (r - 1 + grid->num_rows) & (grid->num_rows - 1);
      c = term->cols - 1;
      if (has_wrapped_around_left(term, r))
        return false;
    }
  }

  const struct row *gr = grid->rows[r];
  if (gr == NULL)
    return false;
  return search_cell_is_word(term, &gr->cells[c]);
}

/* Extract a row's printable cells into a UTF-8 buffer plus a
 * byte-offset → cell-column map. The map has one entry per UTF-8
 * code unit (so byte_to_col[i] is the column the byte at offset i
 * came from). The trailing NUL has the column == term->cols. */
struct row_text {
  char *utf8;
  size_t len;
  int *byte_to_col;
};

static bool extract_row_text(const struct terminal *term, const struct row *row,
                             struct row_text *out) {
  out->utf8 = NULL;
  out->len = 0;
  out->byte_to_col = NULL;

  if (row == NULL)
    return false;

  /* Worst case: every cell becomes 4 UTF-8 bytes for the base char,
   * plus combining chars (cap to a few). */
  size_t cap = (size_t)term->cols * 8 + 1;
  char *buf = xmalloc(cap);
  int *map = xmalloc(cap * sizeof(int));
  size_t pos = 0;

  mbstate_t ps = {0};

  for (int col = 0; col < term->cols; col++) {
    const struct cell *cell = &row->cells[col];
    char32_t base = cell->wc;

    if (base >= CELL_SPACER)
      continue; /* right-half of wide char */

    if (base == 0)
      base = U' ';

    const struct composed *composed = NULL;
    if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) {
      composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
      base = composed->chars[0];
    }

    size_t encoded = c32rtomb(&buf[pos], base, &ps);
    if (encoded == (size_t)-1) {
      buf[pos] = '?';
      encoded = 1;
      memset(&ps, 0, sizeof(ps));
    }
    for (size_t i = 0; i < encoded; i++)
      map[pos + i] = col;
    pos += encoded;

    if (composed != NULL) {
      for (size_t j = 1; j < composed->count; j++) {
        size_t e = c32rtomb(&buf[pos], composed->chars[j], &ps);
        if (e == (size_t)-1) {
          buf[pos] = '?';
          e = 1;
          memset(&ps, 0, sizeof(ps));
        }
        for (size_t i = 0; i < e; i++)
          map[pos + i] = col;
        pos += e;
      }
    }
  }

  buf[pos] = '\0';
  map[pos] = term->cols;

  out->utf8 = buf;
  out->len = pos;
  out->byte_to_col = map;
  return true;
}

static void free_row_text(struct row_text *rt) {
  free(rt->utf8);
  free(rt->byte_to_col);
  rt->utf8 = NULL;
  rt->byte_to_col = NULL;
  rt->len = 0;
}

static bool search_compile_regex(struct terminal *term) {
  /* Free any previously compiled regex */
  if (term->search.regex_compiled != NULL) {
    regfree(term->search.regex_compiled);
    free(term->search.regex_compiled);
    term->search.regex_compiled = NULL;
  }
  term->search.regex_valid = false;

  if (!term->search.regex || term->search.len == 0)
    return false;

  /* Convert search buffer to UTF-8 */
  char pattern[term->search.len * 4 + 1];
  mbstate_t ps = {0};
  size_t out = 0;
  for (size_t i = 0; i < term->search.len; i++) {
    size_t e = c32rtomb(&pattern[out], term->search.buf[i], &ps);
    if (e == (size_t)-1) {
      pattern[out++] = '?';
      memset(&ps, 0, sizeof(ps));
    } else {
      out += e;
    }
  }
  pattern[out] = '\0';

  regex_t *re = xmalloc(sizeof(*re));
  int flags = REG_EXTENDED;
  if (!search_is_case_sensitive(term))
    flags |= REG_ICASE;

  if (regcomp(re, pattern, flags) != 0) {
    free(re);
    return false;
  }

  term->search.regex_compiled = re;
  term->search.regex_valid = true;
  return true;
}

/* Find one regex match on a single row. Returns true if found and
 * fills [start_col, end_col]. */
static bool regex_find_in_row(const struct terminal *term,
                              const struct row *row, int min_col, int max_col,
                              int *out_start, int *out_end) {
  if (!term->search.regex_valid || row == NULL)
    return false;

  struct row_text rt;
  if (!extract_row_text(term, row, &rt))
    return false;

  bool found = false;
  regmatch_t m;
  size_t scan_from = 0;

  /* Find matches; honor min_col by skipping ones that end before it,
   * and max_col by stopping after them. Pick the *first* match whose
   * starting column is in [min_col, max_col]. */
  while (scan_from <= rt.len) {
    if (regexec(term->search.regex_compiled, rt.utf8 + scan_from, 1, &m,
                scan_from > 0 ? REG_NOTBOL : 0) != 0)
      break;
    if (m.rm_so == m.rm_eo) {
      scan_from++;
      continue;
    }

    int s_col = rt.byte_to_col[scan_from + m.rm_so];
    int e_col = rt.byte_to_col[scan_from + m.rm_eo - 1];

    if (s_col >= min_col && s_col <= max_col) {
      if (term->search.whole_word) {
        if ((s_col > 0 && search_cell_is_word(term, &row->cells[s_col - 1])) ||
            (e_col + 1 < term->cols &&
             search_cell_is_word(term, &row->cells[e_col + 1]))) {
          scan_from += m.rm_eo;
          continue;
        }
      }
      *out_start = s_col;
      *out_end = e_col;
      found = true;
      break;
    }

    scan_from += m.rm_eo;
  }

  free_row_text(&rt);
  return found;
}

/* Walk rows in the requested direction, returning the first regex
 * match found within the [start, end] range. */
static bool regex_find_next(const struct terminal *term,
                            enum search_direction direction,
                            struct coord abs_start, struct coord abs_end,
                            struct range *match) {
  const struct grid *grid = term->grid;
  const bool backward = direction != SEARCH_FORWARD;

  int row_no = abs_start.row;
  int from_col = abs_start.col;
  int to_col =
      (row_no == abs_end.row) ? abs_end.col : (backward ? 0 : term->cols - 1);

  while (true) {
    const struct row *row = grid->rows[row_no];
    if (row != NULL) {
      int min_col = backward ? min(from_col, to_col) : from_col;
      int max_col = backward ? max(from_col, to_col) : to_col;

      int s, e;
      if (regex_find_in_row(term, row, min_col, max_col, &s, &e)) {
        match->start = (struct coord){s, row_no};
        match->end = (struct coord){e, row_no};
        return true;
      }
    }

    if (row_no == abs_end.row)
      break;

    if (backward) {
      row_no = (row_no - 1 + grid->num_rows) & (grid->num_rows - 1);
      from_col = term->cols - 1;
      to_col = (row_no == abs_end.row) ? abs_end.col : 0;
    } else {
      row_no = (row_no + 1) & (grid->num_rows - 1);
      from_col = 0;
      to_col = (row_no == abs_end.row) ? abs_end.col : term->cols - 1;
    }
  }

  return false;
}

static ssize_t matches_cell(const struct terminal *term,
                            const struct cell *cell, size_t search_ofs) {
  assert(search_ofs < term->search.len);

  char32_t base = cell->wc;
  const struct composed *composed = NULL;

  if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) {
    composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
    base = composed->chars[0];
  }

  if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ')
    return 1;

  if (search_is_case_sensitive(term)) {
    if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0)
      return -1;
  } else {
    if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0)
      return -1;
  }

  if (composed != NULL) {
    if (search_ofs + composed->count > term->search.len)
      return -1;

    for (size_t j = 1; j < composed->count; j++) {
      if (composed->chars[j] != term->search.buf[search_ofs + j])
        return -1;
    }
  }

  return composed != NULL ? composed->count : 1;
}

static bool find_next(struct terminal *term, enum search_direction direction,
                      struct coord abs_start, struct coord abs_end,
                      struct range *match) {
#define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1))
#define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1))

  if (term->search.regex && term->search.regex_valid)
    return regex_find_next(term, direction, abs_start, abs_end, match);

  struct grid *grid = term->grid;
  const bool backward = direction != SEARCH_FORWARD;

  LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward",
          abs_start.row, abs_start.col, abs_end.row, abs_end.col);

  xassert(abs_start.row >= 0);
  xassert(abs_start.row < grid->num_rows);
  xassert(abs_start.col >= 0);
  xassert(abs_start.col < term->cols);

  xassert(abs_end.row >= 0);
  xassert(abs_end.row < grid->num_rows);
  xassert(abs_end.col >= 0);
  xassert(abs_end.col < term->cols);

  for (int match_start_row = abs_start.row, match_start_col = abs_start.col;;
       backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) {

    const struct row *row = grid->rows[match_start_row];
    if (row == NULL) {
      if (match_start_row == abs_end.row)
        break;
      continue;
    }

    for (; backward ? match_start_col >= 0 : match_start_col < term->cols;
         backward ? match_start_col-- : match_start_col++) {
      if (matches_cell(term, &row->cells[match_start_col], 0) < 0) {
        if (match_start_row == abs_end.row && match_start_col == abs_end.col) {
          break;
        }
        continue;
      }

      /*
       * Got a match on the first letter. Now we'll see if the
       * rest of the search buffer matches.
       */

      LOG_DBG("search: initial match at row=%d, col=%d", match_start_row,
              match_start_col);

      int match_end_row = match_start_row;
      int match_end_col = match_start_col;
      const struct row *match_row = row;
      size_t match_len = 0;

      for (size_t i = 0; i < term->search.len;) {
        if (match_end_col >= term->cols) {
          ROW_INC(match_end_row);
          match_end_col = 0;

          match_row = grid->rows[match_end_row];
          if (match_row == NULL)
            break;
        }

        if (match_row->cells[match_end_col].wc >= CELL_SPACER) {
          match_end_col++;
          continue;
        }

        ssize_t additional_chars =
            matches_cell(term, &match_row->cells[match_end_col], i);
        if (additional_chars < 0)
          break;

        i += additional_chars;
        match_len += additional_chars;
        match_end_col++;

        while (match_end_col < term->cols &&
               match_row->cells[match_end_col].wc > CELL_SPACER) {
          match_end_col++;
        }
      }

      if (match_len != term->search.len) {
        /* Didn't match (completely) */

        if (match_start_row == abs_end.row && match_start_col == abs_end.col) {
          break;
        }

        continue;
      }

      if (term->search.whole_word) {
        /* Reject if neighbour cells are word-chars */
        if (search_neighbor_is_word(term, match_start_row, match_start_col,
                                    false) ||
            search_neighbor_is_word(term, match_end_row, match_end_col - 1,
                                    true)) {
          if (match_start_row == abs_end.row &&
              match_start_col == abs_end.col) {
            break;
          }
          continue;
        }
      }

      *match = (struct range){
          .start = {match_start_col, match_start_row},
          .end = {match_end_col - 1, match_end_row},
      };

      return true;
    }

    if (match_start_row == abs_end.row && match_start_col == abs_end.col)
      break;

    match_start_col = backward ? term->cols - 1 : 0;
  }

  return false;
}

/* Count total matches across the whole grid, capped at
 * SEARCH_COUNT_CAP. Returns the cap if we hit it. */
static size_t search_count_all(struct terminal *term) {
  if (term->search.len == 0)
    return 0;
  if (term->search.regex && !term->search.regex_valid)
    return 0;

  struct grid *grid = term->grid;

  /* Start at the very top of the scrollback (oldest row) */
  int oldest = (grid->offset + term->rows) & (grid->num_rows - 1);

  struct coord pos = {0, oldest};
  struct coord end = {term->cols - 1, oldest};
  /* end.row should be the row *before* oldest, i.e. the newest */
  end.row = (oldest - 1 + grid->num_rows) & (grid->num_rows - 1);

  size_t count = 0;
  while (count < SEARCH_COUNT_CAP) {
    struct range m;
    if (!find_next(term, SEARCH_FORWARD, pos, end, &m))
      break;
    count++;

    /* Advance one cell past the match start */
    pos.col = m.start.col + 1;
    pos.row = m.start.row;
    if (pos.col >= term->cols) {
      pos.col = 0;
      pos.row = (pos.row + 1) & (grid->num_rows - 1);
      if (pos.row == oldest)
        break; /* wrapped */
    }

    if (pos.row == end.row && pos.col > end.col)
      break;
  }

  return count;
}

/* Recompute current_idx by counting matches from the top of the
 * scrollback up to (and including) the current match position. */
static size_t search_compute_current_idx(struct terminal *term) {
  if (term->search.match_len == 0 || term->search.len == 0)
    return 0;
  if (term->search.regex && !term->search.regex_valid)
    return 0;

  struct grid *grid = term->grid;
  int oldest = (grid->offset + term->rows) & (grid->num_rows - 1);

  /* Sentinel "newest cell" - we'll abort when we pass our own match */
  struct coord newest = {term->cols - 1,
                         (oldest - 1 + grid->num_rows) & (grid->num_rows - 1)};

  struct coord pos = {0, oldest};

  size_t idx = 0;
  while (idx < SEARCH_COUNT_CAP) {
    struct range m;
    if (!find_next(term, SEARCH_FORWARD, pos, newest, &m))
      break;
    idx++;

    if (m.start.row == term->search.match.row &&
        m.start.col == term->search.match.col)
      return idx;

    pos.col = m.start.col + 1;
    pos.row = m.start.row;
    if (pos.col >= term->cols) {
      pos.col = 0;
      pos.row = (pos.row + 1) & (grid->num_rows - 1);
      if (pos.row == oldest)
        break;
    }
  }

  return 0;
}

static void search_find_next(struct terminal *term,
                             enum search_direction direction) {
  struct grid *grid = term->grid;

  /* Recompile regex if active (cheap when len==0; no-op otherwise) */
  if (term->search.regex)
    search_compile_regex(term);

  if (term->search.len == 0) {
    term->search.match = (struct coord){-1, -1};
    term->search.match_len = 0;
    term->search.total_count = 0;
    term->search.current_idx = 0;
    term->search.wrapped = false;
    selection_cancel(term);
    return;
  }

  struct coord start = term->search.match;
  size_t len = term->search.match_len;

  xassert((len == 0 && start.row == -1 && start.col == -1) ||
          (len > 0 && start.row >= 0 && start.col >= 0));

  if (len == 0) {
    /* No previous match, start from the top, or bottom, of the scrollback */
    switch (direction) {
    case SEARCH_FORWARD:
      start.row = grid_row_absolute_in_view(grid, 0);
      start.col = 0;
      break;

    case SEARCH_BACKWARD:
    case SEARCH_BACKWARD_SAME_POSITION:
      start.row = grid_row_absolute_in_view(grid, term->rows - 1);
      start.col = term->cols - 1;
      break;
    }
  } else {
    /* Continue from last match */
    xassert(start.row >= 0);
    xassert(start.col >= 0);

    switch (direction) {
    case SEARCH_BACKWARD_SAME_POSITION:
      break;

    case SEARCH_BACKWARD:
      if (--start.col < 0) {
        start.col = term->cols - 1;
        start.row += grid->num_rows - 1;
        start.row &= grid->num_rows - 1;
      }
      break;

    case SEARCH_FORWARD:
      if (++start.col >= term->cols) {
        start.col = 0;
        start.row++;
        start.row &= grid->num_rows - 1;
      }
      break;
    }

    xassert(start.row >= 0);
    xassert(start.row < grid->num_rows);
    xassert(start.col >= 0);
    xassert(start.col < term->cols);
  }

  LOG_DBG("update: %s: starting at row=%d col=%d "
          "(offset = %d, view = %d)",
          direction != SEARCH_FORWARD ? "backward" : "forward", start.row,
          start.col, grid->offset, grid->view);

  struct coord end = start;
  switch (direction) {
  case SEARCH_FORWARD:
    /* Search forward, until we reach the cell *before* current start */
    if (--end.col < 0) {
      end.col = term->cols - 1;
      end.row += grid->num_rows - 1;
      end.row &= grid->num_rows - 1;
    }
    break;

  case SEARCH_BACKWARD:
  case SEARCH_BACKWARD_SAME_POSITION:
    /* Search backwards, until we reach the cell *after* current start */
    if (++end.col >= term->cols) {
      end.col = 0;
      end.row++;
      end.row &= grid->num_rows - 1;
    }
    break;
  }

  /* Remember previous match position for wrap detection */
  const struct coord prev_match = term->search.match;
  const size_t prev_match_len = term->search.match_len;

  struct range match;
  bool found = find_next(term, direction, start, end, &match);

  term->search.wrapped = false;

  if (found) {
    LOG_DBG("primary match found at %dx%d", match.start.row, match.start.col);

    /* Detect wrap: if we had a prior match and the new match is in
     * the "wrong" direction relative to it, we wrapped. */
    if (prev_match_len > 0 && direction != SEARCH_BACKWARD_SAME_POSITION) {
      int oldest = (grid->offset + term->rows) & (grid->num_rows - 1);
      int prev_rebased =
          (prev_match.row - oldest + grid->num_rows) & (grid->num_rows - 1);
      int new_rebased =
          (match.start.row - oldest + grid->num_rows) & (grid->num_rows - 1);

      if (direction == SEARCH_FORWARD) {
        if (new_rebased < prev_rebased ||
            (new_rebased == prev_rebased && match.start.col <= prev_match.col))
          term->search.wrapped = true;
      } else {
        if (new_rebased > prev_rebased ||
            (new_rebased == prev_rebased && match.start.col >= prev_match.col))
          term->search.wrapped = true;
      }
    }

    search_update_selection(term, &match);
    term->search.match = match.start;
    term->search.match_len = term->search.len;
  } else {
    LOG_DBG("no match");
    term->search.match = (struct coord){-1, -1};
    term->search.match_len = 0;
    selection_cancel(term);
  }

  /* Refresh counter */
  term->search.total_count = search_count_all(term);
  term->search.current_idx = search_compute_current_idx(term);
#undef ROW_DEC
}

struct search_match_iterator search_matches_new_iter(struct terminal *term) {
  return (struct search_match_iterator){
      .term = term,
      .start = {0, 0},
  };
}

struct range search_matches_next(struct search_match_iterator *iter) {
  struct terminal *term = iter->term;
  struct grid *grid = term->grid;

  if (term->search.match_len == 0)
    goto no_match;

  if (iter->start.row >= term->rows)
    goto no_match;

  xassert(iter->start.row >= 0);
  xassert(iter->start.row < term->rows);
  xassert(iter->start.col >= 0);
  xassert(iter->start.col < term->cols);

  struct coord abs_start = iter->start;
  abs_start.row = grid_row_absolute_in_view(grid, abs_start.row);

  struct coord abs_end = {term->cols - 1,
                          grid_row_absolute_in_view(grid, term->rows - 1)};

  /* BUG: matches *starting* outside the view, but ending *inside*, aren't
   * matched */
  struct range match;
  bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match);
  if (!found)
    goto no_match;

  LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", match.start.row,
          match.start.col, match.end.row, match.end.col);

  /* Convert absolute row numbers back to view relative */
  match.start.row = match.start.row - grid->view + grid->num_rows;
  match.start.row &= grid->num_rows - 1;
  match.end.row = match.end.row - grid->view + grid->num_rows;
  match.end.row &= grid->num_rows - 1;

  LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d",
          match.start.row, match.start.col, match.end.row, match.end.col,
          grid->view);

  /* Assert match end comes *after* the match start */
  xassert(
      match.end.row > match.start.row ||
      (match.end.row == match.start.row && match.end.col >= match.start.col));

  /* Assert the match starts at, or after, the iterator position */
  xassert(match.start.row > iter->start.row ||
          (match.start.row == iter->start.row &&
           match.start.col >= iter->start.col));

  /* Continue at next column, next time */
  iter->start.row = match.start.row;
  iter->start.col = match.start.col + 1;

  if (iter->start.col >= term->cols) {
    iter->start.col = 0;
    iter->start.row++; /* Overflow is caught in next iteration */
  }

  xassert(iter->start.row >= 0);
  xassert(iter->start.row <= term->rows);
  xassert(iter->start.col >= 0);
  xassert(iter->start.col < term->cols);
  return match;

no_match:
  iter->start.row = -1;
  iter->start.col = -1;
  return (struct range){{-1, -1}, {-1, -1}};
}

static void add_wchars(struct terminal *term, char32_t *src, size_t count) {
  /* Strip non-printable characters */
  for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) {
    if (isc32print(src[i]))
      src[j++] = src[i];
    else
      count--;
  }

  if (!search_ensure_size(term, term->search.len + count))
    return;

  xassert(term->search.len + count < term->search.sz);

  memmove(&term->search.buf[term->search.cursor + count],
          &term->search.buf[term->search.cursor],
          (term->search.len - term->search.cursor) * sizeof(char32_t));

  memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t));

  term->search.len += count;
  term->search.cursor += count;
  term->search.buf[term->search.len] = U'\0';
}

void search_add_chars(struct terminal *term, const char *src, size_t count) {
  size_t chars = mbsntoc32(NULL, src, count, 0);
  if (chars == (size_t)-1) {
    LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src);
    return;
  }

  char32_t c32s[chars + 1];
  mbsntoc32(c32s, src, count, chars);
  add_wchars(term, c32s, chars);
}

enum extend_direction { SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT };

static bool coord_advance_left(const struct terminal *term, struct coord *pos,
                               const struct row **row) {
  const struct grid *grid = term->grid;
  struct coord new_pos = *pos;

  if (--new_pos.col < 0) {
    new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1);
    new_pos.col = term->cols - 1;

    if (has_wrapped_around_left(term, new_pos.row))
      return false;

    if (row != NULL)
      *row = grid->rows[new_pos.row];
  }

  *pos = new_pos;
  return true;
}

static bool coord_advance_right(const struct terminal *term, struct coord *pos,
                                const struct row **row) {
  const struct grid *grid = term->grid;
  struct coord new_pos = *pos;

  if (++new_pos.col >= term->cols) {
    new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1);
    new_pos.col = 0;

    if (has_wrapped_around_right(term, new_pos.row))
      return false;

    if (row != NULL)
      *row = grid->rows[new_pos.row];
  }

  *pos = new_pos;
  return true;
}

static bool search_extend_find_char(const struct terminal *term,
                                    struct coord *target,
                                    enum extend_direction direction) {
  if (term->search.match_len == 0)
    return false;

  struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term)
                                                     : selection_get_end(term);
  xassert(pos.row >= 0);
  xassert(pos.row < term->grid->num_rows);

  *target = pos;

  const struct row *row = term->grid->rows[pos.row];

  while (true) {
    switch (direction) {
    case SEARCH_EXTEND_LEFT:
      if (!coord_advance_left(term, &pos, &row))
        return false;
      break;

    case SEARCH_EXTEND_RIGHT:
      if (!coord_advance_right(term, &pos, &row))
        return false;
      break;
    }

    const char32_t wc = row->cells[pos.col].wc;

    if (wc >= CELL_SPACER || wc == U'\0')
      continue;

    *target = pos;
    return true;
  }
}

static bool search_extend_find_char_left(const struct terminal *term,
                                         struct coord *target) {
  return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT);
}

static bool search_extend_find_char_right(const struct terminal *term,
                                          struct coord *target) {
  return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT);
}

static bool search_extend_find_word(const struct terminal *term,
                                    bool spaces_only, struct coord *target,
                                    enum extend_direction direction) {
  if (term->search.match_len == 0)
    return false;

  struct grid *grid = term->grid;
  struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term)
                                                     : selection_get_end(term);

  xassert(pos.row >= 0);
  xassert(pos.row < grid->num_rows);

  *target = pos;

  /* First character to consider is the *next* character */
  switch (direction) {
  case SEARCH_EXTEND_LEFT:
    if (!coord_advance_left(term, &pos, NULL))
      return false;
    break;

  case SEARCH_EXTEND_RIGHT:
    if (!coord_advance_right(term, &pos, NULL))
      return false;
    break;
  }

  xassert(pos.row >= 0);
  xassert(pos.row < grid->num_rows);
  xassert(grid->rows[pos.row] != NULL);

  /* Find next word boundary */
  switch (direction) {
  case SEARCH_EXTEND_LEFT:
    selection_find_word_boundary_left(term, &pos, spaces_only);
    break;

  case SEARCH_EXTEND_RIGHT:
    selection_find_word_boundary_right(term, &pos, spaces_only, false);
    break;
  }

  *target = pos;
  return true;
}

static bool search_extend_find_word_left(const struct terminal *term,
                                         bool spaces_only,
                                         struct coord *target) {
  return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT);
}

static bool search_extend_find_word_right(const struct terminal *term,
                                          bool spaces_only,
                                          struct coord *target) {
  return search_extend_find_word(term, spaces_only, target,
                                 SEARCH_EXTEND_RIGHT);
}

static bool search_extend_find_line(const struct terminal *term,
                                    struct coord *target,
                                    enum extend_direction direction) {
  if (term->search.match_len == 0)
    return false;

  struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term)
                                                     : selection_get_end(term);

  xassert(pos.row >= 0);
  xassert(pos.row < term->grid->num_rows);

  *target = pos;

  const struct grid *grid = term->grid;

  switch (direction) {
  case SEARCH_EXTEND_LEFT:
    pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1);
    if (has_wrapped_around_left(term, pos.row))
      return false;
    break;

  case SEARCH_EXTEND_RIGHT:
    pos.row = (pos.row + 1) & (grid->num_rows - 1);
    if (has_wrapped_around_right(term, pos.row))
      return false;
    break;
  }

  *target = pos;
  return true;
}

static bool search_extend_find_line_up(const struct terminal *term,
                                       struct coord *target) {
  return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT);
}

static bool search_extend_find_line_down(const struct terminal *term,
                                         struct coord *target) {
  return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT);
}

static void search_extend_left(struct terminal *term,
                               const struct coord *target) {
  if (term->search.match_len == 0)
    return;

  const struct coord last_coord = selection_get_start(term);
  struct coord pos = *target;
  const struct row *row = term->grid->rows[pos.row];

  const bool move_cursor = term->search.cursor != 0;

  struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
  if (ctx == NULL)
    return;

  while (pos.col != last_coord.col || pos.row != last_coord.row) {
    if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx))
      break;
    if (!coord_advance_right(term, &pos, &row))
      break;
  }

  char32_t *new_text;
  size_t new_len;

  if (!extract_finish_wide(ctx, &new_text, &new_len))
    return;

  if (!search_ensure_size(term, term->search.len + new_len))
    return;

  memmove(&term->search.buf[new_len], &term->search.buf[0],
          term->search.len * sizeof(term->search.buf[0]));

  size_t actually_copied = 0;
  for (size_t i = 0; i < new_len; i++) {
    if (new_text[i] == U'\n') {
      /* extract() adds newlines, which we never match against */
      continue;
    }

    term->search.buf[actually_copied++] = new_text[i];
    term->search.len++;
  }

  xassert(actually_copied <= new_len);
  if (actually_copied < new_len) {
    memmove(&term->search.buf[actually_copied], &term->search.buf[new_len],
            (term->search.len - actually_copied) * sizeof(term->search.buf[0]));
  }

  term->search.buf[term->search.len] = U'\0';
  free(new_text);

  if (move_cursor)
    term->search.cursor += actually_copied;

  struct range match = {.start = *target, .end = selection_get_end(term)};
  search_update_selection(term, &match);

  term->search.match_len = term->search.len;
}

static void search_extend_right(struct terminal *term,
                                const struct coord *target) {
  if (term->search.match_len == 0)
    return;

  struct coord pos = selection_get_end(term);
  const struct row *row = term->grid->rows[pos.row];

  const bool move_cursor = term->search.cursor == term->search.len;

  struct extraction_context *ctx = extract_begin(SELECTION_NONE, false);
  if (ctx == NULL)
    return;

  do {
    if (!coord_advance_right(term, &pos, &row))
      break;
    if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx))
      break;
  } while (pos.col != target->col || pos.row != target->row);

  char32_t *new_text;
  size_t new_len;

  if (!extract_finish_wide(ctx, &new_text, &new_len))
    return;

  if (!search_ensure_size(term, term->search.len + new_len))
    return;

  for (size_t i = 0; i < new_len; i++) {
    if (new_text[i] == U'\n') {
      /* extract() adds newlines, which we never match against */
      continue;
    }

    term->search.buf[term->search.len++] = new_text[i];
  }

  term->search.buf[term->search.len] = U'\0';
  free(new_text);

  if (move_cursor)
    term->search.cursor = term->search.len;

  struct range match = {.start = term->search.match, .end = *target};
  search_update_selection(term, &match);
  term->search.match_len = term->search.len;
}

static size_t distance_next_word(const struct terminal *term) {
  size_t cursor = term->search.cursor;

  /* First eat non-whitespace. This is the word we're skipping past */
  while (cursor < term->search.len) {
    if (isc32space(term->search.buf[cursor++]))
      break;
  }

  xassert(cursor == term->search.len ||
          isc32space(term->search.buf[cursor - 1]));

  /* Now skip past whitespace, so that we end up at the beginning of
   * the next word */
  while (cursor < term->search.len) {
    if (!isc32space(term->search.buf[cursor++]))
      break;
  }

  xassert(cursor == term->search.len ||
          !isc32space(term->search.buf[cursor - 1]));

  if (cursor < term->search.len && !isc32space(term->search.buf[cursor]))
    cursor--;

  return cursor - term->search.cursor;
}

static size_t distance_prev_word(const struct terminal *term) {
  int cursor = term->search.cursor;

  /* First, eat whitespace prefix */
  while (cursor > 0) {
    if (!isc32space(term->search.buf[--cursor]))
      break;
  }

  xassert(cursor == 0 || !isc32space(term->search.buf[cursor]));

  /* Now eat non-whitespace. This is the word we're skipping past */
  while (cursor > 0) {
    if (isc32space(term->search.buf[--cursor]))
      break;
  }

  xassert(cursor == 0 || isc32space(term->search.buf[cursor]));
  if (cursor > 0 && isc32space(term->search.buf[cursor]))
    cursor++;

  return term->search.cursor - cursor;
}

static void from_clipboard_cb(char *text, size_t size, void *user) {
  struct terminal *term = user;
  search_add_chars(term, text, size);
}

static void from_clipboard_done(void *user) {
  struct terminal *term = user;

  LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
  search_find_next(term, SEARCH_BACKWARD_SAME_POSITION);
  render_refresh_search(term);
}

static bool execute_binding(struct seat *seat, struct terminal *term,
                            const struct key_binding *binding, uint32_t serial,
                            bool *update_search_result,
                            enum search_direction *direction, bool *redraw) {
  *update_search_result = *redraw = false;
  const enum bind_action_search action = binding->action;

  struct grid *grid = term->grid;

  switch (action) {
  case BIND_ACTION_SEARCH_NONE:
    return false;

  case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, term->rows);
      return true;
    }
    return false;

  case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, max(term->rows / 2, 1));
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, 1);
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, term->rows);
      return true;
    }
    return false;

  case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, max(term->rows / 2, 1));
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, 1);
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_SCROLLBACK_HOME:
    if (term->grid == &term->normal) {
      cmd_scrollback_up(term, term->grid->num_rows);
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_SCROLLBACK_END:
    if (term->grid == &term->normal) {
      cmd_scrollback_down(term, term->grid->num_rows);
      return true;
    }
    break;

  case BIND_ACTION_SEARCH_CANCEL:
    if (term->search.view_followed_offset)
      grid->view = grid->offset;
    else {
      grid->view = ensure_view_is_allocated(term, term->search.original_view);
    }
    term_damage_view(term);
    search_cancel(term);
    return true;

  case BIND_ACTION_SEARCH_COMMIT:
    if (term->search.mode != SEARCH_MODE_NORMAL) {
      session_prompt_commit(term);
      return true;
    }
    selection_finalize(seat, term, serial);
    search_cancel_keep_selection(term);
    return true;

  case BIND_ACTION_SEARCH_FIND_PREV:
    if (term->search.last.buf != NULL && term->search.len == 0) {
      add_wchars(term, term->search.last.buf, term->search.last.len);

      free(term->search.last.buf);
      term->search.last.buf = NULL;
      term->search.last.len = 0;
    }

    *direction = SEARCH_BACKWARD;
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_FIND_NEXT:
    if (term->search.last.buf != NULL && term->search.len == 0) {
      add_wchars(term, term->search.last.buf, term->search.last.len);

      free(term->search.last.buf);
      term->search.last.buf = NULL;
      term->search.last.len = 0;
    }

    *direction = SEARCH_FORWARD;
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_EDIT_LEFT:
    if (term->search.cursor > 0) {
      term->search.cursor--;
      *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: {
    size_t diff = distance_prev_word(term);
    term->search.cursor -= diff;
    xassert(term->search.cursor <= term->search.len);

    if (diff > 0)
      *redraw = true;
    return true;
  }

  case BIND_ACTION_SEARCH_EDIT_RIGHT:
    if (term->search.cursor < term->search.len) {
      term->search.cursor++;
      *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: {
    size_t diff = distance_next_word(term);
    term->search.cursor += diff;
    xassert(term->search.cursor <= term->search.len);

    if (diff > 0)
      *redraw = true;
    return true;
  }

  case BIND_ACTION_SEARCH_EDIT_HOME:
    if (term->search.cursor != 0) {
      term->search.cursor = 0;
      *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_EDIT_END:
    if (term->search.cursor != term->search.len) {
      term->search.cursor = term->search.len;
      *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_DELETE_PREV:
    if (term->search.cursor > 0) {
      memmove(&term->search.buf[term->search.cursor - 1],
              &term->search.buf[term->search.cursor],
              (term->search.len - term->search.cursor) * sizeof(char32_t));
      term->search.cursor--;
      term->search.buf[--term->search.len] = U'\0';
      *update_search_result = *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_DELETE_PREV_WORD: {
    size_t diff = distance_prev_word(term);
    size_t old_cursor = term->search.cursor;
    size_t new_cursor = old_cursor - diff;

    if (diff > 0) {
      memmove(&term->search.buf[new_cursor], &term->search.buf[old_cursor],
              (term->search.len - old_cursor) * sizeof(char32_t));

      term->search.len -= diff;
      term->search.cursor = new_cursor;
      *update_search_result = *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_DELETE_NEXT:
    if (term->search.cursor < term->search.len) {
      memmove(&term->search.buf[term->search.cursor],
              &term->search.buf[term->search.cursor + 1],
              (term->search.len - term->search.cursor - 1) * sizeof(char32_t));
      term->search.buf[--term->search.len] = U'\0';
      *update_search_result = *redraw = true;
    }
    return true;

  case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: {
    size_t diff = distance_next_word(term);
    size_t cursor = term->search.cursor;

    if (diff > 0) {
      memmove(&term->search.buf[cursor], &term->search.buf[cursor + diff],
              (term->search.len - (cursor + diff)) * sizeof(char32_t));

      term->search.len -= diff;
      *update_search_result = *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_DELETE_TO_START: {
    if (term->search.cursor > 0) {
      memmove(&term->search.buf[0], &term->search.buf[term->search.cursor],
              (term->search.len - term->search.cursor) * sizeof(char32_t));

      term->search.len -= term->search.cursor;
      term->search.cursor = 0;
      *update_search_result = *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_DELETE_TO_END: {
    if (term->search.cursor < term->search.len) {
      term->search.buf[term->search.cursor] = '\0';
      term->search.len = term->search.cursor;
      *update_search_result = *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_CHAR: {
    struct coord target;
    if (search_extend_find_char_right(term, &target)) {
      search_extend_right(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_WORD: {
    struct coord target;
    if (search_extend_find_word_right(term, false, &target)) {
      search_extend_right(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_WORD_WS: {
    struct coord target;
    if (search_extend_find_word_right(term, true, &target)) {
      search_extend_right(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: {
    struct coord target;
    if (search_extend_find_line_down(term, &target)) {
      search_extend_right(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: {
    struct coord target;
    if (search_extend_find_char_left(term, &target)) {
      search_extend_left(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: {
    struct coord target;
    if (search_extend_find_word_left(term, false, &target)) {
      search_extend_left(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: {
    struct coord target;
    if (search_extend_find_word_left(term, true, &target)) {
      search_extend_left(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_EXTEND_LINE_UP: {
    struct coord target;
    if (search_extend_find_line_up(term, &target)) {
      search_extend_left(term, &target);
      *update_search_result = false;
      *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_CLIPBOARD_PASTE:
    text_from_clipboard(seat, term, false, &from_clipboard_cb,
                        &from_clipboard_done, term);
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_PRIMARY_PASTE:
    text_from_primary(seat, term, false, &from_clipboard_cb,
                      &from_clipboard_done, term);
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_UNICODE_INPUT:
    unicode_mode_activate(term);
    return true;

  case BIND_ACTION_SEARCH_TOGGLE_CASE:
    term->search.case_mode = (term->search.case_mode + 1) % 3;
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD:
    term->search.whole_word = !term->search.whole_word;
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_TOGGLE_REGEX:
    term->search.regex = !term->search.regex;
    *update_search_result = *redraw = true;
    return true;

  case BIND_ACTION_SEARCH_HISTORY_PREV: {
    struct search_history_entry *target;
    if (term->search.history_pos == NULL)
      target = term->search.history_tail;
    else
      target = term->search.history_pos->prev;

    if (target != NULL) {
      term->search.history_pos = target;
      term->search.len = 0;
      term->search.cursor = 0;
      if (term->search.buf != NULL)
        term->search.buf[0] = U'\0';
      add_wchars(term, target->buf, target->len);
      *update_search_result = *redraw = true;
    }
    return true;
  }

  case BIND_ACTION_SEARCH_HISTORY_NEXT: {
    if (term->search.history_pos == NULL)
      return true;

    struct search_history_entry *target = term->search.history_pos->next;
    term->search.history_pos = target;

    term->search.len = 0;
    term->search.cursor = 0;
    if (term->search.buf != NULL)
      term->search.buf[0] = U'\0';

    if (target != NULL)
      add_wchars(term, target->buf, target->len);

    *update_search_result = *redraw = true;
    return true;
  }

  case BIND_ACTION_SEARCH_COMMIT_LINE: {
    if (term->search.match_len == 0) {
      selection_finalize(seat, term, serial);
      search_cancel_keep_selection(term);
      return true;
    }

    /* Extend selection to span entire line(s) of the match */
    const struct coord match_end = selection_get_end(term);
    const int start_row = term->search.match.row;
    const int end_row = match_end.row;

    int sel_start_row = start_row - grid->view + grid->num_rows;
    sel_start_row &= grid->num_rows - 1;

    int sel_end_row = end_row - grid->view + grid->num_rows;
    sel_end_row &= grid->num_rows - 1;

    selection_cancel(term);
    selection_start(term, 0, sel_start_row, SELECTION_CHAR_WISE, false);
    selection_update(term, term->cols - 1, sel_end_row);
    selection_finalize(seat, term, serial);
    search_cancel_keep_selection(term);
    return true;
  }

  case BIND_ACTION_SEARCH_COUNT:
    BUG("Invalid action type");
    return true;
  }

  BUG("Unhandled action type");
  return false;
}

void search_input(struct seat *seat, struct terminal *term,
                  const struct key_binding_set *bindings, uint32_t key,
                  xkb_keysym_t sym, xkb_mod_mask_t mods,
                  xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms,
                  size_t raw_count, uint32_t serial) {
  LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym,
          mods, consumed);

  /* Overwrite-confirm: only y/Y proceeds; anything else cancels. */
  if (term->search.mode == SEARCH_MODE_SESSION_OVERWRITE_CONFIRM) {
    if (sym == XKB_KEY_y || sym == XKB_KEY_Y || sym == XKB_KEY_Return ||
        sym == XKB_KEY_KP_Enter) {
      session_prompt_confirm_overwrite(term);
    } else {
      session_prompt_cancel(term);
    }
    return;
  }

  /* In session-load mode, hijack a few keys for picker navigation. We do this
   * before binding dispatch so configured search-bindings (e.g. history-prev
   * on Up) don't fire. */
  if (term->search.mode == SEARCH_MODE_SESSION_LOAD) {
    bool handled = true;
    switch (sym) {
    case XKB_KEY_Up:
      session_picker_move(term, -1);
      break;
    case XKB_KEY_Down:
      session_picker_move(term, +1);
      break;
    case XKB_KEY_Page_Up:
      session_picker_move(term, -5);
      break;
    case XKB_KEY_Page_Down:
      session_picker_move(term, +5);
      break;
    case XKB_KEY_Delete:
      session_picker_delete_selected(term);
      break;
    default:
      handled = false;
      break;
    }
    if (handled) {
      render_refresh_search(term);
      return;
    }
  }

  enum xkb_compose_status compose_status =
      seat->kbd.xkb_compose_state != NULL
          ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state)
          : XKB_COMPOSE_NOTHING;

  enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION;
  bool update_search_result = false;
  bool redraw = false;

  /*
   * Key bindings
   */

  /* Match untranslated symbols */
  tll_foreach(bindings->search, it) {
    const struct key_binding *bind = &it->item;

    if (bind->mods != mods || bind->mods == 0)
      continue;

    for (size_t i = 0; i < raw_count; i++) {
      if (bind->k.sym == raw_syms[i]) {
        if (execute_binding(seat, term, bind, serial, &update_search_result,
                            &search_direction, &redraw)) {
          goto update_search;
        }
        return;
      }
    }
  }

  /* Match translated symbol */
  tll_foreach(bindings->search, it) {
    const struct key_binding *bind = &it->item;

    if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) {

      if (execute_binding(seat, term, bind, serial, &update_search_result,
                          &search_direction, &redraw)) {
        goto update_search;
      }
      return;
    }
  }

  /* Match raw key code */
  tll_foreach(bindings->search, it) {
    const struct key_binding *bind = &it->item;

    if (bind->mods != mods || bind->mods == 0)
      continue;

    tll_foreach(bind->k.key_codes, code) {
      if (code->item == key) {
        if (execute_binding(seat, term, bind, serial, &update_search_result,
                            &search_direction, &redraw)) {
          goto update_search;
        }
        return;
      }
    }
  }

  uint8_t buf[64] = {0};
  int count = 0;

  if (compose_status == XKB_COMPOSE_COMPOSED) {
    count = xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, (char *)buf,
                                       sizeof(buf));
    xkb_compose_state_reset(seat->kbd.xkb_compose_state);
  } else if (compose_status == XKB_COMPOSE_CANCELLED ||
             compose_status == XKB_COMPOSE_COMPOSING) {
    count = 0;
  } else {
    count = xkb_state_key_get_utf8(seat->kbd.xkb_state, key, (char *)buf,
                                   sizeof(buf));
  }

  update_search_result = redraw = count > 0;
  search_direction = SEARCH_BACKWARD_SAME_POSITION;

  if (count == 0)
    return;

  search_add_chars(term, (const char *)buf, count);

update_search:
  LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf);
  if (update_search_result && term->search.mode == SEARCH_MODE_NORMAL)
    search_find_next(term, search_direction);
  if (term->search.mode == SEARCH_MODE_SESSION_LOAD)
    session_picker_refilter(term);
  if (redraw)
    render_refresh_search(term);
}
