#define _DARWIN_C_SOURCE 1
#define _XOPEN_SOURCE 700
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <termios.h>
#include <unistd.h>
#include <wchar.h>

#define KEY_CTRL(c) ((c) & 31)
#define TABSTOP 8

enum {
    KEY_NONE = 256, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN,
    KEY_HOME, KEY_END, KEY_DELETE, KEY_PAGEUP, KEY_PAGEDOWN, KEY_RESIZE
};
struct row { char *data; size_t len; };
struct frame { char *data; size_t len, cap; };
static struct {
    struct row *rows;
    size_t count, capacity, x, y, top, left, goal;
    int width, height, dirty, raw, screen;
    mode_t new_mode;
    char *path;
    const char *name;
    char message[512];
    struct termios original;
    struct frame frame;
} ed;
static volatile sig_atomic_t resized, stopped;

static int write_all(int fd, const char *data, size_t len)
{
    while (len) {
        size_t chunk = len > (size_t)SSIZE_MAX ? (size_t)SSIZE_MAX : len;
        ssize_t n = write(fd, data, chunk);
        if (n < 0 && errno == EINTR) continue;
        if (n <= 0) { if (!n) errno = EIO; return -1; }
        data += n;
        len -= (size_t)n;
    }
    return 0;
}

static void restore_terminal(void)
{
    if (ed.raw) {
        int rc;
        do { rc = tcsetattr(STDIN_FILENO, TCSAFLUSH, &ed.original); }
        while (rc < 0 && errno == EINTR);
        ed.raw = 0;
        if (rc < 0) perror("editor: restore terminal");
    }
    if (ed.screen) {
        const char reset[] = "\x1b[0m\x1b[?25h\x1b[?1049l";
        ed.screen = 0;
        if (write_all(STDOUT_FILENO, reset, sizeof reset - 1) < 0)
            perror("editor: restore screen");
    }
}

static void cleanup(void)
{
    restore_terminal();
    for (size_t i = 0; i < ed.count; i++) free(ed.rows[i].data);
    free(ed.rows);
    free(ed.path);
    free(ed.frame.data);
}

static void fatal(const char *what)
{
    int error = errno;
    restore_terminal();
    fprintf(stderr, "editor: %s: %s\n", what, strerror(error));
    exit(EXIT_FAILURE);
}

static void *resize(void *p, size_t count, size_t size)
{
    if (size && count > SIZE_MAX / size) { errno = ENOMEM; fatal("allocation"); }
    size_t bytes = count * size;
    void *q = realloc(p, bytes ? bytes : 1);
    if (!q) fatal("allocation");
    return q;
}

static void message(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(ed.message, sizeof ed.message, fmt, ap);
    va_end(ap);
}

static void on_signal(int signo)
{
    if (signo == SIGWINCH) resized = 1;
    else stopped = signo;
}

static void window_size(void)
{
    struct winsize ws;
    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) fatal("window size");
    if (!ws.ws_row || !ws.ws_col) { errno = EINVAL; fatal("window size"); }
    ed.width = ws.ws_col;
    ed.height = ws.ws_row;
    resized = 0;
}

static void start_terminal(void)
{
    struct sigaction sa = {0};
    int signals[] = {SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGPIPE, SIGWINCH};
    sa.sa_handler = on_signal;
    sigemptyset(&sa.sa_mask);
    for (size_t i = 0; i < sizeof signals / sizeof signals[0]; i++)
        if (sigaction(signals[i], &sa, NULL) < 0) fatal("sigaction");
    /* Suspension is deliberately unsupported: do not leave a stopped raw tty. */
    sa.sa_handler = SIG_IGN;
    if (sigaction(SIGTSTP, &sa, NULL) < 0) fatal("sigaction");
    if (tcgetattr(STDIN_FILENO, &ed.original) < 0) fatal("tcgetattr");
    struct termios raw = ed.original;
    raw.c_iflag &= ~(tcflag_t)(BRKINT | ICRNL | INLCR | IGNCR | INPCK | ISTRIP | IXON | PARMRK);
    raw.c_oflag &= ~(tcflag_t)OPOST;
    raw.c_cflag = (raw.c_cflag & ~(tcflag_t)CSIZE) | CS8;
    raw.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 1;
    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) fatal("raw mode");
    ed.raw = 1;
    ed.screen = 1;
    const char enter[] = "\x1b[?1049h\x1b[2J\x1b[H";
    if (write_all(STDOUT_FILENO, enter, sizeof enter - 1) < 0) fatal("screen output");
}

/* VTIME bounds escape-sequence reads and lets signals wake the main loop. */
static int read_byte(void)
{
    unsigned char c;
    for (;;) {
        if (stopped) return -1;
        ssize_t n = read(STDIN_FILENO, &c, 1);
        if (n == 1) return c;
        if (!n || (n < 0 && errno == EINTR)) return -1;
        fatal("terminal input");
    }
}

static int read_key(void)
{
    int c;
    do {
        if (stopped) return KEY_NONE;
        if (resized) return KEY_RESIZE;
        c = read_byte();
    } while (c < 0);
    if (c != 27) return c;
    c = read_byte();
    if (c < 0) return 27;
    if (c == ']' || c == 'P' || c == '^' || c == '_') {
        int previous = 0;
        while ((c = read_byte()) >= 0) {
            if (c == 7 || (previous == 27 && c == '\\')) break;
            previous = c;
        }
        return KEY_NONE;
    }
    if (c != '[' && c != 'O') return KEY_NONE;
    char sequence[32];
    size_t len = 0;
    int overflow = 0;
    while ((c = read_byte()) >= 0) {
        if (len < sizeof sequence - 1) sequence[len++] = (char)c;
        else overflow = 1;
        if (c >= 0x40 && c <= 0x7e) break;
    }
    if (c < 0 || overflow) return KEY_NONE;
    sequence[len] = '\0';
    switch (c) {
    case 'A': return KEY_UP;
    case 'B': return KEY_DOWN;
    case 'C': return KEY_RIGHT;
    case 'D': return KEY_LEFT;
    case 'H': return KEY_HOME;
    case 'F': return KEY_END;
    case '~':
        switch (strtol(sequence, NULL, 10)) {
        case 1: case 7: return KEY_HOME;
        case 4: case 8: return KEY_END;
        case 3: return KEY_DELETE;
        case 5: return KEY_PAGEUP;
        case 6: return KEY_PAGEDOWN;
        }
    }
    return KEY_NONE;
}

static void add_row(size_t at, const char *text, size_t len)
{
    if (len == SIZE_MAX || ed.count == SIZE_MAX) { errno = ENOMEM; fatal("row size"); }
    struct row row = {resize(NULL, len + 1, 1), len};
    memcpy(row.data, text, len);
    row.data[len] = '\0';
    if (ed.count == ed.capacity) {
        size_t capacity = ed.capacity ? ed.capacity * 2 : 16;
        if (capacity < ed.capacity) { errno = ENOMEM; fatal("row count"); }
        ed.rows = resize(ed.rows, capacity, sizeof *ed.rows);
        ed.capacity = capacity;
    }
    memmove(ed.rows + at + 1, ed.rows + at, (ed.count - at) * sizeof *ed.rows);
    ed.rows[at] = row;
    ed.count++;
}

static void load_file(void)
{
    int fd = open(ed.name, O_RDONLY | O_NONBLOCK);
    if (fd < 0) {
        if (errno != ENOENT) fatal(ed.name);
        struct stat st;
        /* A dangling symlink is not a new regular file. */
        if (lstat(ed.name, &st) == 0) { errno = ELOOP; fatal(ed.name); }
        if (errno != ENOENT) fatal(ed.name);
        ed.path = resize(NULL, strlen(ed.name) + 1, 1);
        strcpy(ed.path, ed.name);
        add_row(0, "", 0);
        message("New file | Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
        return;
    }
    struct stat st;
    if (fstat(fd, &st) < 0) fatal(ed.name);
    if (!S_ISREG(st.st_mode)) { errno = EINVAL; fatal("not a regular file"); }
    FILE *file = fdopen(fd, "rb");
    if (!file) fatal("fdopen");
    ed.path = realpath(ed.name, NULL);
    if (!ed.path) fatal(ed.name);
    char *line = NULL;
    size_t cap = 0;
    ssize_t n;
    int final_newline = 0;
    while ((n = getline(&line, &cap, file)) >= 0) {
        final_newline = n > 0 && line[n - 1] == '\n';
        add_row(ed.count, line, (size_t)n - (size_t)final_newline);
    }
    free(line);
    if (ferror(file)) { if (!errno) errno = EIO; fatal("read file"); }
    if (fclose(file) == EOF) fatal("close file");
    /* An empty final row preserves a final newline without inventing one. */
    if (!ed.count || final_newline) add_row(ed.count, "", 0);
    message("Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
}

static void save_file(void)
{
    struct stat st;
    mode_t mode = ed.new_mode;
    int fd, error = 0, remove_error = 0;
    size_t n = strlen(ed.path);
    if (n > SIZE_MAX - 8) { errno = ENOMEM; fatal("filename"); }
    char *temp = resize(NULL, n + 8, 1);
    snprintf(temp, n + 8, "%s.XXXXXX", ed.path);
    if (lstat(ed.path, &st) == 0) {
        if (!S_ISREG(st.st_mode)) { error = EINVAL; goto done; }
        mode = st.st_mode & 0777;
        /* Respect the target's write permissions before replacing its inode. */
        if (access(ed.path, W_OK) < 0) { error = errno; goto done; }
    } else if (errno != ENOENT) { error = errno; goto done; }
    fd = mkstemp(temp);
    if (fd < 0) { error = errno; goto done; }
    for (size_t i = 0; i < ed.count; i++) {
        if (write_all(fd, ed.rows[i].data, ed.rows[i].len) < 0 ||
            (i + 1 < ed.count && write_all(fd, "\n", 1) < 0)) {
            error = errno;
            break;
        }
    }
    if (!error && fchmod(fd, mode) < 0) error = errno;
    if (!error && fsync(fd) < 0) error = errno;
    if (close(fd) < 0 && !error) error = errno;
    if (!error && rename(temp, ed.path) < 0) error = errno;
    if (error && unlink(temp) < 0) remove_error = errno;
done:
    if (remove_error) message("Save failed: %s; temporary file remains: %s", strerror(error), temp);
    else if (error) message("Save failed: %s", strerror(error));
    else { ed.dirty = 0; message("Saved %s", ed.name); }
    free(temp);
}

/* Decode one character for cursor movement and display. Invalid bytes are kept
 * in the file but shown as '?'; controls use caret notation and tabs use spaces. */
static size_t character(const char *s, size_t len, size_t column, int *width, int *valid)
{
    unsigned char c = (unsigned char)*s;
    *valid = 1;
    if (c == '\t') { *width = TABSTOP - (int)(column % TABSTOP); return 1; }
    if (c < 32 || c == 127) { *width = 2; return 1; }
    if (c < 128) { *width = 1; return 1; }
    mbstate_t state = {0};
    wchar_t wc;
    size_t n = mbrtowc(&wc, s, len, &state);
    if (n == (size_t)-1 || n == (size_t)-2 || !n || (*width = wcwidth(wc)) < 0) {
        *width = 1;
        *valid = 0;
        return 1;
    }
    return n;
}

static size_t previous_char(const char *s, size_t len, size_t at)
{
    if (!at) return 0;
    size_t start = at - 1;
    while (start && at - start < MB_LEN_MAX && ((unsigned char)s[start] & 0xc0) == 0x80)
        start--;
    int width, valid;
    size_t n = character(s + start, len - start, 0, &width, &valid);
    return start + n == at ? start : at - 1;
}

static size_t column_at(const struct row *row, size_t at)
{
    size_t x = 0, column = 0;
    while (x < at) {
        int width, valid;
        x += character(row->data + x, row->len - x, column, &width, &valid);
        column += (size_t)width;
    }
    return column;
}

static size_t offset_at(const struct row *row, size_t goal)
{
    size_t x = 0, column = 0;
    while (x < row->len) {
        int width, valid;
        size_t n = character(row->data + x, row->len - x, column, &width, &valid);
        if (column + (size_t)width > goal) break;
        column += (size_t)width;
        x += n;
    }
    return x;
}

static void insert_byte(int c)
{
    struct row *row = &ed.rows[ed.y];
    if (row->len > SIZE_MAX - 2) { errno = ENOMEM; fatal("row size"); }
    row->data = resize(row->data, row->len + 2, 1);
    memmove(row->data + ed.x + 1, row->data + ed.x, row->len - ed.x + 1);
    row->data[ed.x++] = (char)c;
    row->len++;
    ed.dirty = 1;
}

static void split_row(void)
{
    struct row *row = &ed.rows[ed.y];
    add_row(ed.y + 1, row->data + ed.x, row->len - ed.x);
    row = &ed.rows[ed.y];
    row->len = ed.x;
    row->data[ed.x] = '\0';
    ed.y++;
    ed.x = 0;
    ed.dirty = 1;
}

static void delete_forward(void)
{
    struct row *row = &ed.rows[ed.y];
    if (ed.x < row->len) {
        int width, valid;
        size_t n = character(row->data + ed.x, row->len - ed.x, 0, &width, &valid);
        memmove(row->data + ed.x, row->data + ed.x + n, row->len - ed.x - n + 1);
        row->len -= n;
    } else if (ed.y + 1 < ed.count) {
        struct row *next = row + 1;
        if (next->len >= SIZE_MAX - row->len) { errno = ENOMEM; fatal("row size"); }
        row->data = resize(row->data, row->len + next->len + 1, 1);
        memcpy(row->data + row->len, next->data, next->len + 1);
        row->len += next->len;
        free(next->data);
        memmove(next, next + 1, (ed.count - ed.y - 2) * sizeof *next);
        ed.count--;
    } else return;
    ed.dirty = 1;
}

static int text_height(void)
{
    return ed.height > 2 ? ed.height - 2 : 1;
}

static void move_cursor(int key)
{
    struct row *row = &ed.rows[ed.y];
    if (key == KEY_UP || key == KEY_DOWN || key == KEY_PAGEUP || key == KEY_PAGEDOWN) {
        size_t amount = (key == KEY_UP || key == KEY_DOWN) ? 1 : (size_t)text_height();
        if (ed.goal == SIZE_MAX) ed.goal = column_at(row, ed.x);
        if (key == KEY_UP || key == KEY_PAGEUP)
            ed.y = amount > ed.y ? 0 : ed.y - amount;
        else ed.y += amount < ed.count - 1 - ed.y ? amount : ed.count - 1 - ed.y;
        ed.x = offset_at(&ed.rows[ed.y], ed.goal);
        return;
    }
    ed.goal = SIZE_MAX;
    switch (key) {
    case KEY_LEFT:
        if (ed.x) ed.x = previous_char(row->data, row->len, ed.x);
        else if (ed.y) ed.x = ed.rows[--ed.y].len;
        break;
    case KEY_RIGHT:
        if (ed.x < row->len) {
            int width, valid;
            ed.x += character(row->data + ed.x, row->len - ed.x, 0, &width, &valid);
        } else if (ed.y + 1 < ed.count) { ed.y++; ed.x = 0; }
        break;
    case KEY_HOME: ed.x = 0; break;
    case KEY_END: ed.x = row->len; break;
    }
}

static void emit(const char *s, size_t len)
{
    struct frame *f = &ed.frame;
    if (len > SIZE_MAX - f->len) { errno = ENOMEM; fatal("screen size"); }
    size_t needed = f->len + len;
    if (needed > f->cap) {
        size_t cap = needed <= SIZE_MAX / 2 ? needed * 2 : needed;
        f->data = resize(f->data, cap, 1);
        f->cap = cap;
    }
    memcpy(f->data + f->len, s, len);
    f->len = needed;
}

static void sequence(const char *s)
{
    emit(s, strlen(s));
}

static void position(int y, int x)
{
    char s[64];
    int n = snprintf(s, sizeof s, "\x1b[%d;%dH", y, x);
    emit(s, (size_t)n);
}

static void draw_text(const char *s, size_t len, size_t left)
{
    size_t x = 0, column = 0, end = left + (size_t)ed.width;
    int base_visible = 0;
    while (x < len && column < end) {
        int width, valid;
        size_t n = character(s + x, len - x, column, &width, &valid);
        size_t after = column + (size_t)width;
        if (!width) {
            if (base_visible) emit(s + x, n);
        } else if (after > left) {
            size_t first = column < left ? left : column;
            size_t last = after > end ? end : after;
            unsigned char c = (unsigned char)s[x];
            base_visible = column >= left && after <= end;
            if (c == '\t' || !base_visible) {
                for (size_t i = first; i < last; i++) sequence(" ");
            } else if (c < 32 || c == 127) {
                char control[2] = {'^', c == 127 ? '?' : (char)(c + 64)};
                emit(control, 2);
            } else if (!valid) sequence("?");
            else emit(s + x, n);
        } else base_visible = 0;
        column = after;
        x += n;
    }
    sequence("\x1b[K");
}

static void refresh(void)
{
    if (resized) window_size();
    ed.frame.len = 0;
    sequence("\x1b[?25l");
    if (ed.height < 3 || ed.width < 10) {
        sequence("\x1b[H\x1b[2J");
        draw_text("Window too small", 16, 0);
        position(1, 1);
    } else {
        size_t column = column_at(&ed.rows[ed.y], ed.x);
        size_t height = (size_t)text_height();
        if (ed.y < ed.top) ed.top = ed.y;
        if (ed.y - ed.top >= height) ed.top = ed.y - height + 1;
        if (column < ed.left) ed.left = column;
        if (column - ed.left >= (size_t)ed.width) ed.left = column - (size_t)ed.width + 1;
        for (int screen_y = 0; screen_y < text_height(); screen_y++) {
            size_t y = ed.top + (size_t)screen_y;
            position(screen_y + 1, 1);
            if (y < ed.count) draw_text(ed.rows[y].data, ed.rows[y].len, ed.left);
            else sequence("~\x1b[K");
        }
        char status[512], details[96];
        const char *name = strrchr(ed.name, '/');
        name = name ? name + 1 : ed.name;
        snprintf(details, sizeof details, " | %zu lines | %zu:%zu",
                 ed.count, ed.y + 1, column + 1);
        int room = ed.width - (int)strlen(details) - 4;
        snprintf(status, sizeof status, "%s %.*s%s", ed.dirty ? "[+]" : "[ ]",
                 room > 0 ? room : 0, name, details);
        position(ed.height - 1, 1);
        sequence("\x1b[7m");
        draw_text(status, strlen(status), 0);
        sequence("\x1b[0m");
        position(ed.height, 1);
        draw_text(ed.message, strlen(ed.message), 0);
        position((int)(ed.y - ed.top) + 1, (int)(column - ed.left) + 1);
    }
    sequence("\x1b[?25h");
    if (write_all(STDOUT_FILENO, ed.frame.data, ed.frame.len) < 0) fatal("screen output");
}

/* Search bytes, including past embedded NULs. Forward/backward search wraps. */
static int find_match(const char *query, size_t len, int direction, int include)
{
    size_t chosen_y = SIZE_MAX, chosen_x = 0, wrap_y = SIZE_MAX, wrap_x = 0;
    for (size_t y = 0; y < ed.count; y++) {
        const struct row *row = &ed.rows[y];
        if (len > row->len) continue;
        for (size_t x = 0; x <= row->len - len; x++) {
            if (memcmp(row->data + x, query, len)) continue;
            int equal = y == ed.y && x == ed.x;
            int eligible = direction > 0 ? (y > ed.y || (y == ed.y && x > ed.x)) :
                                           (y < ed.y || (y == ed.y && x < ed.x));
            if (wrap_y == SIZE_MAX || direction < 0) { wrap_y = y; wrap_x = x; }
            if (eligible || (include && equal)) {
                chosen_y = y; chosen_x = x;
                if (direction > 0) goto found;
            }
        }
    }
found:
    if (chosen_y == SIZE_MAX) { chosen_y = wrap_y; chosen_x = wrap_x; }
    if (chosen_y == SIZE_MAX) return 0;
    ed.y = chosen_y;
    ed.x = chosen_x;
    return 1;
}

static void search(void)
{
    char query[256] = "";
    size_t len = 0, x = ed.x, y = ed.y, top = ed.top, left = ed.left;
    int found = 1;
    while (!stopped) {
        message("Search: %s%s | arrows next/prev, Enter accept, Esc cancel",
                query, found ? "" : " [not found]");
        refresh();
        int key = read_key();
        if (key == 27 || key == KEY_CTRL('q')) {
            ed.x = x; ed.y = y; ed.top = top; ed.left = left;
            message("Search cancelled");
            return;
        }
        if (key == '\r' || key == '\n') { message("Search: %s", query); return; }
        if (key == KEY_UP || key == KEY_LEFT || key == KEY_DOWN || key == KEY_RIGHT) {
            if (len) found = find_match(query, len, key == KEY_UP || key == KEY_LEFT ? -1 : 1, 0);
            continue;
        }
        if (key == 127 || key == KEY_CTRL('h')) {
            if (len) len = previous_char(query, len, len);
        } else if ((key >= 32 && key < 256) || key == '\t') {
            if (len + 1 < sizeof query) query[len++] = (char)key;
        } else continue;
        query[len] = '\0';
        ed.x = x; ed.y = y;
        found = !len || find_match(query, len, 1, 1);
    }
}

int main(int argc, char **argv)
{
    if (argc != 2) { fprintf(stderr, "usage: %s file\n", argv[0]); return 1; }
    if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
        fprintf(stderr, "editor: standard input and output must be terminals\n");
        return 1;
    }
    if (atexit(cleanup)) { fprintf(stderr, "editor: cannot register cleanup\n"); return 1; }
    setlocale(LC_CTYPE, "");
    ed.name = argv[1];
    ed.goal = SIZE_MAX;
    mode_t mask = umask(0);
    umask(mask);
    ed.new_mode = 0666 & ~mask;
    load_file();
    window_size();
    start_terminal();
    int confirm_quit = 0;
    while (!stopped) {
        refresh();
        int key = read_key();
        if (key == KEY_NONE || key == KEY_RESIZE) continue;
        if (key == KEY_CTRL('q')) {
            if (!ed.dirty || confirm_quit) break;
            message("Unsaved changes. Ctrl-Q again discards them; Ctrl-S saves.");
            confirm_quit = 1;
            continue;
        }
        if (confirm_quit) message("Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
        confirm_quit = 0;
        switch (key) {
        case KEY_CTRL('s'): save_file(); break;
        case KEY_CTRL('f'): search(); break;
        case '\r': case '\n': split_row(); break;
        case 127: case KEY_CTRL('h'):
            if (ed.x || ed.y) { move_cursor(KEY_LEFT); delete_forward(); }
            break;
        case KEY_DELETE: delete_forward(); break;
        case KEY_LEFT: case KEY_RIGHT: case KEY_UP: case KEY_DOWN:
        case KEY_HOME: case KEY_END: case KEY_PAGEUP: case KEY_PAGEDOWN:
            move_cursor(key);
            continue;
        default:
            if (key == '\t' || (key >= 32 && key < 256)) insert_byte(key);
        }
        ed.goal = SIZE_MAX;
    }
    return stopped ? 128 + stopped : 0;
}
