/*
 * resize.c
 * Copyright (C) 2024 Kovid Goyal <kovid at kovidgoyal.net>
 *
 * Distributed under terms of the GPL3 license.
 */

#include "resize.h"
#include "lineops.h"

typedef struct Rewrap {
    struct {
        LineBuf *lb;
        HistoryBuf *hb;
        index_type x, y, hb_count;
        Line line, scratch_line;
    } src, dest;
    ANSIBuf *as_ansi_buf;
    TrackCursor *cursors;
    LineBuf *sb;

    index_type num_content_lines_before, src_x_limit;
    bool prev_src_line_ended_with_wrap, current_src_line_has_multline_cells, current_dest_line_has_multiline_cells;
    bool dest_line_from_linebuf, src_is_in_linebuf;

} Rewrap;

static void
setup_line(TextCache *tc, index_type xnum, Line *l) {
    l->text_cache = tc;
    l->xnum = xnum;
}

#define src_xnum (r->src.lb->xnum)
#define dest_xnum (r->dest.lb->xnum)

static void
exclude_empty_lines_at_bottom(Rewrap *r) {
    index_type first, i;
    bool is_empty = true;
    // Find the first line that contains some content
#define self (r->src.lb)
    first = self->ynum;
    do {
        first--;
        CPUCell *cells = linebuf_cpu_cells_for_line(self, first);
        for(i = 0; i < self->xnum; i++) {
            if (cells[i].ch_or_idx || cells[i].ch_is_idx) { is_empty = false; break; }
        }
    } while(is_empty && first > 0);
    if (!is_empty) r->num_content_lines_before = first + 1;
#undef self
}

static void
init_src_line_basic(Rewrap *r, index_type y, Line *dest, bool update_state) {
    if (r->src_is_in_linebuf) {
        linebuf_init_line_at(r->src.lb, y - r->src.hb_count, dest);
    } else if (y >= r->src.hb_count) {
        if (update_state) r->src_is_in_linebuf = true;
        linebuf_init_line_at(r->src.lb, y - r->src.hb_count, dest);
    } else {
        // historybuf_init_line uses reverse indexing
        historybuf_init_line(r->src.hb, r->src.hb->count - y - 1, dest);
    }
}

static bool
init_src_line(Rewrap *r) {
    bool newline_needed = !r->prev_src_line_ended_with_wrap;
    init_src_line_basic(r, r->src.y, &r->src.line, true);
    r->src_x_limit = src_xnum;
    r->prev_src_line_ended_with_wrap = r->src.line.cpu_cells[src_xnum - 1].next_char_was_wrapped;
    r->src.line.cpu_cells[src_xnum - 1].next_char_was_wrapped = false;
    // Trim trailing blanks
    while (r->src_x_limit && r->src.line.cpu_cells[r->src_x_limit - 1].ch_and_idx == BLANK_CHAR) r->src_x_limit--;
    r->src.x = 0;
    r->current_src_line_has_multline_cells = false;
    for (index_type i = 0; i < r->src_x_limit; i++) if (r->src.line.cpu_cells[i].is_multicell && r->src.line.cpu_cells[i].scale > 1) {
        r->current_src_line_has_multline_cells = true;
        break;
    }
    return newline_needed;
}

#define set_dest_line_attrs(dest_y) r->dest.lb->line_attrs[dest_y] = r->src.line.attrs; r->src.line.attrs.prompt_kind = UNKNOWN_PROMPT_KIND;

static void
first_dest_line(Rewrap *r) {
    if (r->src.hb_count) {
        historybuf_next_dest_line(r->dest.hb, r->as_ansi_buf, &r->src.line, 0, &r->dest.line, false);
        r->src.line.attrs.prompt_kind = UNKNOWN_PROMPT_KIND;
    } else {
        r->dest_line_from_linebuf = true;
        linebuf_init_line_at(r->dest.lb, 0, &r->dest.line);
        set_dest_line_attrs(0);
    }
}

static index_type
linebuf_next_dest_line(Rewrap *r, bool continued) {
#define dest_y r->dest.y
    LineBuf *dest = r->dest.lb;
    linebuf_set_last_char_as_continuation(dest, dest_y, continued);
    if (dest_y >= dest->ynum - 1) {
        linebuf_index(dest, 0, dest->ynum - 1);
        if (r->dest.hb != NULL) {
            linebuf_init_line(dest, dest->ynum - 1);
            dest->line->attrs.has_dirty_text = true;
            historybuf_add_line(r->dest.hb, dest->line, r->as_ansi_buf);
        }
        linebuf_clear_line(dest, dest->ynum - 1, true);
    } else dest_y++;
    linebuf_init_line_at(dest, dest_y, &r->dest.line);
    set_dest_line_attrs(dest_y);
    return dest_y;
#undef dest_y
}


static void
next_dest_line(Rewrap *r, bool continued) {
    r->dest.x = 0;
    r->current_dest_line_has_multiline_cells = false;
    if (r->dest_line_from_linebuf) {
        r->dest.y = linebuf_next_dest_line(r, continued);
    } else if (r->src_is_in_linebuf) {
        r->dest_line_from_linebuf = true;
        r->dest.y = 0;
        linebuf_init_line_at(r->dest.lb, 0, &r->dest.line);
        set_dest_line_attrs(0);
        if (continued && r->dest.hb && r->dest.hb->count) {
            historybuf_init_line(r->dest.hb, 0, r->dest.hb->line);
            r->dest.hb->line->cpu_cells[dest_xnum-1].next_char_was_wrapped = true;
        }
    } else {
        r->dest.y = historybuf_next_dest_line(r->dest.hb, r->as_ansi_buf, &r->src.line, r->dest.y, &r->dest.line, continued);
        r->src.line.attrs.prompt_kind = UNKNOWN_PROMPT_KIND;
    }
    if (r->sb->line_attrs[0].has_dirty_text) {
        CPUCell *cpu_cells; GPUCell *gpu_cells;
        linebuf_init_cells(r->sb, 0, &cpu_cells, &gpu_cells);
        memcpy(r->dest.line.cpu_cells, cpu_cells, dest_xnum * sizeof(cpu_cells[0]));
        memcpy(r->dest.line.gpu_cells, gpu_cells, dest_xnum * sizeof(gpu_cells[0]));
        r->current_dest_line_has_multiline_cells = true;
    }
    linebuf_index(r->sb, 0, r->sb->ynum - 1);
    if (r->sb->line_attrs[r->sb->ynum - 1].has_dirty_text) {
        linebuf_clear_line(r->sb, r->sb->ynum - 1, true);
    }
}

static void
update_tracked_cursors(Rewrap *r, index_type num_cells, index_type src_y, index_type dest_y, index_type x_limit) {
    if (!r->src_is_in_linebuf) return;
    src_y -= r->src.hb_count;
    for (TrackCursor *t = r->cursors; !t->is_sentinel; t++) {
        if (t->y == src_y && r->src.x <= t->x && (t->x < r->src.x + num_cells || t->x >= x_limit)) {
            t->dest_y = dest_y;
            t->dest_x = r->dest.x + (t->x - r->src.x);
            if (t->dest_x > dest_xnum) t->dest_x = dest_xnum;
        }
    }
}

static bool
find_space_in_dest_line(Rewrap *r, index_type num_cells) {
    while (r->dest.x + num_cells <= dest_xnum) {
        index_type before = r->dest.x;
        for (index_type x = r->dest.x; x < r->dest.x + num_cells; x++) {
            if (r->dest.line.cpu_cells[x].is_multicell) {
                r->dest.x = x + mcd_x_limit(r->dest.line.cpu_cells + x);
                break;
            }
        }
        if (before == r->dest.x) return true;
    }
    return false;
}

static void
find_space_in_dest(Rewrap *r, index_type num_cells) {
    while (!find_space_in_dest_line(r, num_cells)) next_dest_line(r, true);
}

static void
copy_range(Line *src, index_type src_at, Line* dest, index_type dest_at, index_type num) {
    memcpy(dest->cpu_cells + dest_at, src->cpu_cells + src_at, num * sizeof(CPUCell));
    memcpy(dest->gpu_cells + dest_at, src->gpu_cells + src_at, num * sizeof(GPUCell));
}

static void
copy_multiline_extra_lines(Rewrap *r, CPUCell *src_cell, index_type mc_width) {
    for (index_type i = 1; i < src_cell->scale; i++) {
        init_src_line_basic(r, r->src.y + i, &r->src.scratch_line, false);
        linebuf_init_line_at(r->sb, i - 1, &r->dest.scratch_line);
        linebuf_mark_line_dirty(r->sb, i - 1);
        copy_range(&r->src.scratch_line, r->src.x, &r->dest.scratch_line, r->dest.x, mc_width);
        update_tracked_cursors(r, mc_width, r->src.y + i, r->dest.y + i, src_xnum + 10000 /* ensure cursor is moved only if in region being copied */);
    }
}


static void
multiline_copy_src_to_dest(Rewrap *r) {
    CPUCell *c; index_type mc_width;
    while (r->src.x < r->src_x_limit) {
        c = &r->src.line.cpu_cells[r->src.x];
        if (c->is_multicell) {
            mc_width = mcd_x_limit(c);
            if (mc_width > dest_xnum) {
                update_tracked_cursors(r, mc_width, r->src.y, r->dest.y, r->src_x_limit);
                r->src.x += mc_width;
                continue;
            } else if (c->y) {
                r->src.x += mc_width;
                continue;
            }
        } else mc_width = 1;
        find_space_in_dest(r, mc_width);
        copy_range(&r->src.line, r->src.x, &r->dest.line, r->dest.x, mc_width);
        update_tracked_cursors(r, mc_width, r->src.y, r->dest.y, r->src_x_limit);
        if (c->scale > 1) copy_multiline_extra_lines(r, c, mc_width);
        r->src.x += mc_width; r->dest.x += mc_width;
    }
}


static void
fast_copy_src_to_dest(Rewrap *r) {
    CPUCell *c;
    while (r->src.x < r->src_x_limit) {
        if (r->dest.x >= dest_xnum) {
            next_dest_line(r, true);
            if (r->current_dest_line_has_multiline_cells) {
                multiline_copy_src_to_dest(r);
                return;
            }
        }
        index_type num = MIN(r->src_x_limit - r->src.x, dest_xnum - r->dest.x);
        if (num && (c = &r->src.line.cpu_cells[r->src.x + num - 1])->is_multicell && c->x != mcd_x_limit(c) - 1) {
            // we have a split multicell at the right edge of the copy region
            multiline_copy_src_to_dest(r);
            return;
        }
        copy_range(&r->src.line, r->src.x, &r->dest.line, r->dest.x, num);
        update_tracked_cursors(r, num, r->src.y, r->dest.y, r->src_x_limit);
        r->src.x += num; r->dest.x += num;
    }
}


static void
rewrap(Rewrap *r) {
    r->src.hb_count = r->src.hb ? r->src.hb->count : 0;
    // Fast path
    if (r->dest.lb->xnum == r->src.lb->xnum && r->dest.lb->ynum == r->src.lb->ynum) {
        memcpy(r->dest.lb->line_map, r->src.lb->line_map, sizeof(index_type) * r->src.lb->ynum);
        memcpy(r->dest.lb->line_attrs, r->src.lb->line_attrs, sizeof(LineAttrs) * r->src.lb->ynum);
        memcpy(r->dest.lb->cpu_cell_buf, r->src.lb->cpu_cell_buf, (size_t)r->src.lb->xnum * r->src.lb->ynum * sizeof(CPUCell));
        memcpy(r->dest.lb->gpu_cell_buf, r->src.lb->gpu_cell_buf, (size_t)r->src.lb->xnum * r->src.lb->ynum * sizeof(GPUCell));
        r->num_content_lines_before = r->src.lb->ynum;
        if (r->dest.hb && r->src.hb) historybuf_fast_rewrap(r->dest.hb, r->src.hb);
        r->dest.y = r->src.lb->ynum - 1;
        return;
    }

    setup_line(r->src.lb->text_cache, src_xnum, &r->src.line);
    setup_line(r->src.lb->text_cache, dest_xnum, &r->dest.line);
    setup_line(r->src.lb->text_cache, src_xnum, &r->src.scratch_line);
    setup_line(r->src.lb->text_cache, dest_xnum, &r->dest.scratch_line);

    exclude_empty_lines_at_bottom(r);

    for (; r->src.y < r->num_content_lines_before + r->src.hb_count; r->src.y++) {
        if (init_src_line(r)) {
            if (r->src.y) next_dest_line(r, false);
            else first_dest_line(r);
        }
        if (r->current_src_line_has_multline_cells || r->current_dest_line_has_multiline_cells) multiline_copy_src_to_dest(r);
        else fast_copy_src_to_dest(r);
    }
}

ResizeResult
resize_screen_buffers(LineBuf *lb, HistoryBuf *hb, index_type lines, index_type columns, ANSIBuf *as_ansi_buf, TrackCursor *cursors) {
    ResizeResult ans = {0};
    ans.lb = alloc_linebuf(lines, columns, lb->text_cache);
    if (!ans.lb) return ans;
    RAII_PyObject(raii_nlb, (PyObject*)ans.lb); (void) raii_nlb;
    if (hb) {
        ans.hb = historybuf_alloc_for_rewrap(columns, hb);
        if (!ans.hb) return ans;
    }
    RAII_PyObject(raii_nhb, (PyObject*)ans.hb); (void) raii_nhb;
    Rewrap r = {
        .src = {.lb=lb, .hb=hb}, .dest = {.lb=ans.lb, .hb=ans.hb},
        .as_ansi_buf = as_ansi_buf, .cursors = cursors,
    };
    r.sb = alloc_linebuf(SCALE_BITS << 1, columns, lb->text_cache);
    if (!r.sb) return ans;
    RAII_PyObject(scratch, (PyObject*)r.sb); (void)scratch;
    for (TrackCursor *t = cursors; !t->is_sentinel; t++) { t->dest_x = t->x; t->dest_y = t->y; }
    rewrap(&r);
    ans.num_content_lines_before = r.num_content_lines_before;
    ans.num_content_lines_after = MIN(r.dest.y + 1, ans.lb->ynum);
    if (hb) historybuf_finish_rewrap(ans.hb, hb);
    for (unsigned i = 0; i < ans.num_content_lines_after; i++) linebuf_mark_line_dirty(ans.lb, i);
    for (TrackCursor *t = cursors; !t->is_sentinel; t++) { t->dest_x = MIN(t->dest_x, columns); t->dest_y = MIN(t->dest_y, lines); }
    Py_INCREF(raii_nlb); Py_XINCREF(raii_nhb);
    ans.ok = true;
    return ans;
}

static void
nuke_in_line(CPUCell *cp, GPUCell *gp, index_type start, index_type x_limit) {
    for (index_type x = start; x < x_limit; x++) {
        cell_set_char(cp + x, 0); cp[x].is_multicell = false;
        clear_sprite_position(gp[x]);
    }
}

static void
nuke_multicell_char_at(LineBuf *lb, index_type x_, index_type y_) {
    CPUCell *cp; GPUCell *gp;
    linebuf_init_cells(lb, y_, &cp, &gp);
    index_type num_lines_above = cp[x_].y;
    index_type y_max_limit = MIN(lb->ynum, y_ + cp[x_].scale - num_lines_above);
    while (cp[x_].x && x_ > 0) x_--;
    index_type x_limit = MIN(lb->xnum, x_ + mcd_x_limit(&cp[x_]));
    for (index_type y = y_; y < y_max_limit; y++) {
        linebuf_init_cells(lb, y, &cp, &gp);
        nuke_in_line(cp, gp, x_, x_limit);
    }
    for (int y = (int)y_ - 1; y > -1 && num_lines_above; y--, num_lines_above--) {
        linebuf_init_cells(lb, y, &cp, &gp);
        nuke_in_line(cp, gp, x_, x_limit);
    }
}


ResizeResult
resize_screen_buffer_without_rewrap(LineBuf *lb, index_type lines, index_type columns, TrackCursor *cursors) {
    ResizeResult ans = {0};
    ans.lb = alloc_linebuf(lines, columns, lb->text_cache);
    if (!ans.lb) return ans;
    Rewrap r = { .src = {.lb=lb},};
    exclude_empty_lines_at_bottom(&r);
    ans.num_content_lines_before = r.num_content_lines_before;
    ans.num_content_lines_after = MIN(lines, r.num_content_lines_before);

    index_type xcommon = MIN(lb->xnum, ans.lb->xnum);
    for (index_type y = 0; y < ans.num_content_lines_after; y++) {
        linebuf_init_line(lb, y); linebuf_init_line(ans.lb, y);
        ans.lb->line_attrs[y] = lb->line_attrs[y]; ans.lb->line_attrs[y].has_dirty_text = true;
        memcpy(ans.lb->line->cpu_cells, lb->line->cpu_cells, xcommon * sizeof(lb->line->cpu_cells[0]));
        memcpy(ans.lb->line->gpu_cells, lb->line->gpu_cells, xcommon * sizeof(lb->line->gpu_cells[0]));
        if (xcommon > lb->line->xnum) {
            // extend the colors/styles of the last cell to edge
            GPUCell e = lb->line->gpu_cells[xcommon-1]; clear_sprite_position(e);
            for (index_type x = xcommon; x < ans.lb->line->xnum; x++) ans.lb->line->gpu_cells[x] = e;
        } else if (xcommon < lb->line->xnum) {
            // remove multicell chars that were split at the right edge
            index_type last_x = xcommon - 1;
            CPUCell *c = ans.lb->line->cpu_cells + last_x;
            if (c->is_multicell && c->x + 1u < mcd_x_limit(c)) {
                while (ans.lb->line->cpu_cells[last_x].x && last_x > 0) last_x--;
                nuke_in_line(ans.lb->line->cpu_cells, ans.lb->line->gpu_cells, last_x, ans.lb->line->xnum);
            }
        }
    }
    // Set bg color for extra lines at bottom
    if (ans.num_content_lines_before < lines) {
        linebuf_init_line(lb, lb->ynum-1); GPUCell *g = lb->line->gpu_cells;
        for (index_type y = ans.num_content_lines_after; y < ans.lb->ynum; y++) {
            linebuf_init_line(ans.lb, y);
            for (index_type x = 0; x < ans.lb->xnum; x++) ans.lb->line->gpu_cells[x].bg = g->bg;
        }
    } else if (ans.num_content_lines_after < ans.num_content_lines_before) {
        // delete multicell chars split at the bottom
        linebuf_init_line(ans.lb, ans.num_content_lines_after-1);
        for (index_type x = 0; x < ans.lb->xnum; x++) {
            CPUCell *c = ans.lb->line->cpu_cells + x;
            if (c->is_multicell && c->y < c->scale-1) nuke_multicell_char_at(ans.lb, x, ans.num_content_lines_after-1);
        }
    }
    for (TrackCursor *tc = cursors; !tc->is_sentinel; tc++) {
        tc->dest_x = MIN(tc->x, ans.lb->xnum-1);
        tc->dest_y = MIN(tc->y, ans.lb->ynum-1);
    }
    ans.ok = true;
    return ans;
}
