/*
 * mdview2_md.c — представление MD-документа (peer к mdview2_raw.c): отрисовка
 * области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key().
 * Статус-бар, меню и спиннер — в mdview2_status.c (общие для MD/RAW).
 *
 * Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и
 * состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
 * парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша
 * через win_rest_remap (ремап 8-битных кодировок — в mdview2_enc.c).
 *
 * Пара видов: mdview2_md.c (MD, из кэша) ↔ mdview2_raw.c (RAW, из fb()).
 */

#include <stdint.h>
#include <conio.h>          /* wrchar/scroll/COLOR */
#include <bios/text.h>      /* bios_set_place/bios_fillcharattr */
#include "mdview2.h"

/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
 * единого обращения к fb()/исходному файлу. Горизонтальный скролл для
 * nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2
 * байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся
 * строка лежит в одной странице, так что off+vx*2 не пересекает
 * границу страницы); не nowrap-строки игнорируют viewport_x, как и
 * раньше в живом render_line() (там это называлось effective_vx). */
static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
{
    cache_rec_t rec;
    cache_dir_get(line_idx, &rec);

    /* Строка-обрыв (исчерпан контент-кэш / лимит строк): фикс. текст вживую,
     * без обращения к контент-кэшу (см. index_lines финализацию). */
    if (rec.flags & IF_TRUNC_MSG) {
        const char *msg;
        switch (rec.reserved) {
            case TRUNC_LINES: msg = "*** Line limit reached - document truncated ***";       break;
            case TRUNC_FILE:  msg = "*** File too large - truncated at 256 KB ***";           break;
            default:          msg = "*** Content cache exhausted - document truncated ***";   break;
        }
        bios_set_place(row, 0);
        bios_fillcharattr(' ', ATTR_WARN, SCREEN_W);
        put_str_attr(2, row, msg, ATTR_WARN);
        return;
    }

    bios_set_place(row, 0);
    bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W);

    /* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код,
     * таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая
     * строки короче 80 (короткая строка, ушедшая за левый край, просто
     * показывает маркер '<' на пустом месте). HR и границы fence не имеют
     * IF_HSCROLL и не сдвигаются. */
    uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0;

    if (rec.len > vx) {
        uint16_t remaining = (uint16_t)(rec.len - vx);
        uint8_t  w         = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining;
        uint16_t off        = (uint16_t)(rec.off + (uint16_t)vx * 2u);
        win_rest_remap(row, w, rec.page, off);
        if (remaining > SCREEN_W) {
            wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
        }
    }
    if (vx > 0) {
        wrchar(0, row, '<', ATTR_TRUNC);
    }
}

/* Перерисовывает все VIEW_H строк окна из кэша — основной путь
 * перерисовки видимой области после загрузки файла (Фаза 3-4). */
void draw_viewport_from_cache(void)
{
    for (uint8_t i = 0; i < VIEW_H; i++) {
        draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
    }
}


/* ==================================================================
 *                          Прокрутка
 * ================================================================== */

/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */
void clamp_top(void)
{
    if (n_lines <= VIEW_H) {
        top_line = 0;
    } else if (top_line > n_lines - VIEW_H) {
        top_line = (uint16_t)(n_lines - VIEW_H);
    }
}

/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
void md_scroll_up(uint16_t n)
{
    uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
    if( new_top_line != top_line) {
        top_line = new_top_line;
        if (n == 1) {
            scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0);
            draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW));
        } else {
            draw_viewport_from_cache();
        }
    }
}

/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
void md_scroll_down(uint16_t n)
{
    uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
            (uint16_t)(top_line + n) : n_lines - VIEW_H;
    if( new_top_line != top_line) {
        top_line = new_top_line;
        if (n == 1) {
            scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0);
            draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
        } else {
            clamp_top();
            draw_viewport_from_cache();
        }
    }
}

/* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
 * Максимум сдвига ограничен самой широкой nowrap-строкой на экране.
 * Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже
 * посчитанная реальная длина в ячейках после рендера), без единого
 * обращения к fb()/seg_off()/исходному файлу. */
void md_scroll_horizon(int8_t delta)
{
    uint16_t maxw = 0;
    for (uint8_t i = 0; i < VIEW_H; i++) {
        uint16_t li = (uint16_t)(top_line + i);
        if (li >= n_lines) break;
        cache_rec_t rec;
        cache_dir_get(li, &rec);
        if (!(rec.flags & IF_HSCROLL)) continue;        /* только код/таблицы */
        if (rec.len > maxw) maxw = rec.len;             /* самая широкая строка блока */
    }
    if (maxw == 0) return;                              /* нет строк шире экрана */

    /* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */
    uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
    if (over > 248u) over = 248u;
    uint8_t max_vx = (uint8_t)over;

    int16_t nx = (int16_t)viewport_x + delta;
    if (nx < 0) nx = 0;
    if (nx > (int16_t)max_vx) nx = max_vx;

    /* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже
     * сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для
     * `if (new_vx != viewport_x) viewport_x = new_vx;` такой код:
     *   ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a
     * — т.е. для записи переиспользует регистр A, испорченный вычитанием в
     * сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx).
     * (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно
     * наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем
     * портящий sub с пути записи; сравнение ниже только решает, перерисовывать
     * ли экран. */
    uint8_t new_vx = (uint8_t)nx;
    uint8_t old_vx = viewport_x;
    viewport_x = new_vx;
    if (new_vx != old_vx)
        draw_viewport_from_cache();
}

/* Процент прокрутки (0..100) относительно доступного диапазона. */
static uint8_t calc_md_pct(void)
{
    uint16_t total = drawable_lines();
    if (total <= VIEW_H) return 100;
    return pct16(top_line, (uint16_t)(total - VIEW_H));
}

/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */

/* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного
 * цикла). Сама перерисовывает область/статус. Возврат: 1 — обработано,
 * 0 — не наша клавиша (F1/F8/F10 разбирает main). */
uint8_t md_key(uint8_t scan)
{
    switch (scan) {
        case KEY_UP:
            md_scroll_up(1);
            break;
        case KEY_DOWN:
            md_scroll_down(1);
            break;
        case KEY_LEFT:
            md_scroll_horizon(-(int8_t)HPAN_STEP);
            break;
        case KEY_RIGHT:
            md_scroll_horizon(+(int8_t)HPAN_STEP);
            break;
        case KEY_PGUP:
            md_scroll_up(VIEW_H);
            break;
        case KEY_PGDN:
            md_scroll_down(VIEW_H);
            break;
        case KEY_HOME:
            if(top_line != 0 || viewport_x != 0 ) {
                top_line = 0;
                viewport_x = 0;
                draw_viewport_from_cache();
            }
            break;
        case KEY_END:
            uint16_t new_top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0;
            if(top_line != new_top_line || viewport_x != 0 ) {
                top_line = new_top_line;
                viewport_x = 0;
                draw_viewport_from_cache();
            }
            break;

        default:
            return 0;                      /* не наша клавиша */
    }

    render_percent_progress(calc_md_pct());
    render_md_status_numbers();
    return 1;
}
