/*
 * mdview2 — просмотрщик Markdown для Sprinter (render-cache версия).
 *
 * Этот файл — ЯДРО (инфраструктура + точка входа). Прикладные подсистемы
 * вынесены в отдельные модули (общий интерфейс — mdview2.h):
 *     mdview2_index.c — парсер/индексатор markdown (построение рендер-кэша)
 *     mdview2_md.c    — представление MD-документа (отрисовка из кэша + скролл)
 *     mdview2_raw.c   — RAW-просмотр исходного текста (F2/F3)
 *     mdview2_enc.c   — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер
 *     mdview2_table.c — выровненная отрисовка таблиц
 *     mdview2_help.c  — диалог справки (F1)
 *
 * В ядре остаётся: пул рендер-кэша, доступ к EMM/окну W3 (fb/win_rest),
 * примитивы экрана, загрузка файла, наборы документа (doc-slots), кооператив-
 * ная loading-loop и диспетчер клавиш, сборка наборов и main().
 *
 *   Разметка экрана (80x32):
 *     строка 0      — статус-бар (имя файла / диапазон строк / всего / %)
 *     строки 1..30  — область документа (30 видимых строк текста)
 *     строка 31     — меню (F1 Help / F2 RAW / F3 Wrap / F8 CodePg / F10 Exit)
 *
 *   Память:
 *     CODE             → W1   (режим sprinter-cc --memory small)
 *     STACK/HEAP/DATA  → W2
 *     Буфер файла      → страницы EMM (до 8 × 16 КБ = 128 КБ),
 *                        отображаемые в W3 по требованию через fb().
 */

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprinter_exit.h>
#include <palette.h>
#include "mdview2.h"     /* геометрия, атрибуты, общий интерфейс модулей */

/* ---- Палитра атрибутов ------------------------------------------- */

static const uint8_t md_pallete[16 * 4] = {
    0x00, 0x00, 0x00, 0x00,
    0x60, 0x00, 0x00, 0x00,
    0x00, 0x60, 0x00, 0x00,
    0x60, 0x60, 0x00, 0x00,
    0x00, 0x00, 0x60, 0x00,
    0x60, 0x00, 0x60, 0x00,
    0x00, 0x60, 0x60, 0x00,
    0xC8, 0xC8, 0xC8, 0x00,
    0xA0, 0xA0, 0x80, 0x00,
    0xFF, 0xAA, 0xAA, 0x00,
    0xAA, 0xFF, 0xAA, 0x00,
    0xFF, 0xFF, 0xAA, 0x00,
    0xAA, 0xAA, 0xFF, 0x00,
    0xFF, 0xAA, 0xFF, 0x00,
    0xAA, 0xFF, 0xFF, 0x00,
    0xFF, 0xFF, 0xFF, 0xFF
};

/* Атрибуты экрана/справки и геометрия — в mdview2.h (общие со всеми модулями).
 * Типы записей (idx_rec_t — в mdview2_index.c, cache_rec_t — в mdview2.h),
 * словари флагов/стилей (IF_, CK_, INIT_STYLE_) и константы индекса
 * (INDEX_RECS_PER_PAGE, MAX_CACHE_LINE_LEN) — в mdview2.h.
 *
 * В SDCC/z80 критично явно инициализировать static-переменные (`= 0`):
 * неинициализированные static-объекты могут конфликтовать по размещению
 * и перезаписывать друг друга (см. memory/sdcc_static_storage_gotcha). */

#define MAX_INDEX_PAGES      9u      /* максимум 9 * 2048 = 18432 сегментов индекса (256 КБ) */

/* ==================================================================
 *         Рендер-кэш: директория и пул EMM-страниц
 * ==================================================================
 * Каждая логическая строка экрана рендерится РОВНО ОДИН РАЗ (Фаза 2)
 * в готовый буфер и больше никогда не парсится повторно при скролле.
 * Содержимое живёт в ОТДЕЛЬНОМ пуле EMM-страниц (cache_content_*),
 * не путать с idx_rec_t/index_phys[] выше — те остаются неизменными,
 * ими пользуется рендер-воркер ПРИ построении кэша (Фаза 2), и их
 * нельзя перезатирать, т.к. воркер для одной строки может заглядывать
 * в idx-записи СОСЕДНИХ строк (откат cont-сегментов).
 *
 * ЕДИНЫЙ формат для ВСЕХ строк (решено 2026-06-23 — изначальный план
 * различал тип 1/тип 2 по экономии памяти, но строки таблиц — IF_NOWRAP,
 * но НЕ IF_CODE — всё равно проходят inline-парсер и могут содержать
 * несколько атрибутов, так что "один стиль" для них неверно; бюджет EMM
 * (см. sprinter_emm_budget) с большим запасом покрывает (char,attr)
 * для всех строк без исключения): контент — `len` пар (char,attr),
 * `len` — реальная длина в ЯЧЕЙКАХ без паддинга до SCREEN_W, капается на
 * 255 (патологически длинные nowrap-строки урезаются; индикатор обрезки
 * добавляется на этапе вывода из кэша, не при рендере в кэш).
 */
/* cache_rec_t (РОВНО 8 байт, см. sprinter_winrest_format) — в mdview2.h. */

#define CACHE_DIR_REC_SIZE       ((uint16_t)sizeof(cache_rec_t))
#define CACHE_DIR_RECS_PER_PAGE  (16384 / CACHE_DIR_REC_SIZE) /* 2048 */
#define MAX_CACHE_DIR_PAGES      9u    /* симметрично индексу: 9 * 2048 = 18432 записей */
/* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */
#define MAX_CACHE_CONTENT_PAGES  40u    /* кап контента на набор: 40 * 16 КБ = 640 КБ (80 на оба набора) */

static uint8_t  cache_dir_blk;
static uint8_t  cache_dir_pages;
static uint8_t  cache_dir_phys[MAX_CACHE_DIR_PAGES];

static uint8_t  cache_content_blk[MAX_CACHE_CONTENT_PAGES]; /* id блока на каждую выделенную страницу */
static uint8_t  cache_content_phys[MAX_CACHE_CONTENT_PAGES];
static uint8_t  cache_content_n_pages;   /* сколько страниц уже выделено */
static uint8_t  cache_cur_page_idx;   /* индекс текущей страницы записи в cache_content_phys */
static uint16_t cache_cur_off;   /* текущее смещение записи в этой странице */

/* Доступ к директории рендер-кэша — тот же приём, что у idx_get/idx_put. */
void cache_dir_get(uint16_t idx, cache_rec_t *r)
{
    uint8_t  page = (uint8_t)(idx >> 11);
    uint16_t off  = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */
    bank_read(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE);
}

void cache_dir_put(uint16_t idx, const cache_rec_t *r)
{
    uint8_t  page = (uint8_t)(idx >> 11);
    uint16_t off  = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */
    bank_write(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE);
}

/* Резервирует nbytes подряд в пуле контента рендер-кэша.
 * Запись никогда не разбивается через границу страницы: если остаток
 * текущей страницы недостаточен — переходим на следующую целиком,
 * жертвуя остатком (проще и безопаснее, чем page-crossing в fb()).
 * При нехватке текущего пула — лениво доращиваем его на 1 страницу.
 * Возврат: 1 — успех (out_page и out_off заполнены), 0 — EMM кончилась. */
uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off)
{
    if (nbytes == 0 || nbytes > PAGE_SIZE) return 0;

    if (cache_content_n_pages == 0 ||
        (uint16_t)(cache_cur_off + nbytes) > PAGE_SIZE) {
        /* Нужна новая страница: либо пул ещё пуст, либо текущая страница
         * не вмещает запись целиком. */
        uint8_t next_idx = (cache_content_n_pages == 0) ? 0 : (uint8_t)(cache_cur_page_idx + 1);
        if (next_idx >= cache_content_n_pages) {
            if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES) 
                return 0;
            uint8_t blk = mem_alloc_pages(1);
            if (blk == 0) 
                return 0;
            cache_content_blk[cache_content_n_pages]  = blk;
            cache_content_phys[cache_content_n_pages] = mem_get_page(blk, 0);
            cache_content_n_pages++;
        }
        cache_cur_page_idx = next_idx;
        cache_cur_off       = 0;
    }

    *out_page = cache_content_phys[cache_cur_page_idx];
    *out_off  = cache_cur_off;
    cache_cur_off = (uint16_t)(cache_cur_off + nbytes);
    return 1;
}

/* Копирует уже готовый буфер (рендер одной строки, собранный в W1/W2 —
 * НЕ в W3, чтобы не конфликтовать с fb()/чтением исходника во время
 * рендера) в зарезервированное место в пуле контента. Один bank_write
 * на строку, а не по байту. len — РОВНО как у cache_reserve()/bank_write
 * — uint16_t, не uint8_t: строка до MAX_CACHE_LINE_LEN=255 ячеек даёт
 * до 510 байт (char,attr), это уже не лезет в uint8_t. */
void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len)
{
    bank_write(page, off, buf, len);
}

/* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */
/* emph_to_attr — в mdview2_index.c. */

uint16_t  n_lines;                        /* extern: всего строк в индексе текущего набора */
uint16_t  max_lines;                        /* extern: ёмкость индекса: index_pages * 2048 */
uint16_t  top_line;                        /* extern: верхняя видимая строка */
uint32_t  file_size;                        /* extern: размер активного буфера документа */
static uint8_t   file_blk;
static uint8_t   file_pages;
static uint8_t   file_phys[MAX_PAGES];
uint8_t   index_blk;                        /* extern: дескриптор EMM-блока индекса */
uint8_t   index_pages;                      /* */
uint8_t   index_phys[MAX_INDEX_PAGES];    /* extern: физ. страницы индекса */
uint8_t   index_truncated;                  /* выставляется при исчерпании ёмкости индекса */
uint8_t   g_file_clamped;                  /* extern: файл >256 КБ обрезан до MAX_FILE при загрузке */

/* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */
uint8_t   cur_page  = 0xFF;             /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */
uint8_t   viewport_x;               /* extern: горизонтальный сдвиг для nowrap-строк */
char      filename[64];           /* extern: статус-бар */

/* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока
 * index_lines() ещё досканирует файл (кооперативно, без прерываний). */
uint8_t          g_view         = VIEW_MD;          /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */
#if WITH_RAW
uint8_t          g_ready;                /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */
static uint8_t   g_raw_last     = VIEW_RAW_WRAP;    /* последний под-режим RAW (возврат по F2) */
#if WITH_HEX
static uint8_t   g_hex_last     = VIEW_MD;          /* вид, из которого вошли в HEX (возврат по F4) */
#endif
#endif
uint8_t          g_loading;                /* extern: 1 во время видимой индексации (build_doc) */
static uint8_t   g_first_drawn;                /* первый экран уже нарисован */
uint8_t          g_f8_enabled;                /* extern: можно ли сейчас переключать кодировку */
static uint8_t   g_utf_failed;                /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */
uint8_t          g_bg_building;                /* extern: 1 во время фонового index_step — гасит спиннер индексатора */
static uint8_t   g_bg_doc = 0xFF;              /* слот, строящийся в фоне (bg_step); 0xFF = фоновой сборки нет */
uint8_t          g_abort;                /* extern: F10/Esc во время загрузки → прервать индексацию */
static uint16_t  g_last_prog;                /* n_lines на последнем обновлении статуса */

/* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */
const uint8_t   *g_remap;                       /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/
uint8_t          g_scratch_phys;                /* extern: EMM-страница под ремап-буфер строки для win_rest */

/* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его
 * состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h.
 * build_doc() ниже перенаправляет живой file_* на utf_phys и оркеструет
 * инкрементальную конвертацию (utf_conv_reset/utf_convert_more). */

/* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его
 * как есть, набор DOC_UTF8 читает utf_phys. */
static uint8_t  orig_file_blk;
uint8_t         orig_file_pages;                /* extern: читает кодек (utf_alloc) */
uint32_t        orig_file_size;                /* extern: читает кодек (utf_convert_more) */
uint8_t         orig_file_phys[MAX_PAGES];    /* extern: читает кодек (cv_read) */

/* ----------------- Два сосуществующих набора индекс/кэш -----------------
 * DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на
 * отрисовке); DOC_UTF8 — UTF-8, декодированный в CP866. Оба строятся eager
 * при старте (кооперативно), поэтому F8 переключает активный набор мгновенно.
 * fb()/idx/cache-аксессоры работают по «живым» глобалам; doc_switch() свапает
 * их с сохранённой копией нужного набора. */
typedef struct {
    uint8_t  built;
    uint8_t  enc;                     /* кодировка отображения этого набора */
    uint8_t  file_blk, file_pages;
    uint32_t file_size;
    uint8_t  file_phys[MAX_PAGES];
    uint8_t  index_blk, index_pages, index_truncated;
    uint16_t n_lines, max_lines;
    uint8_t  index_phys[MAX_INDEX_PAGES];
    uint8_t  cache_dir_blk, cache_dir_pages;
    uint8_t  cache_dir_phys[MAX_CACHE_DIR_PAGES];
    uint8_t  cache_content_n_pages, cache_cur_page_idx;
    uint16_t cache_cur_off;
    uint8_t  cache_content_blk[MAX_CACHE_CONTENT_PAGES];
    uint8_t  cache_content_phys[MAX_CACHE_CONTENT_PAGES];
    uint16_t top_line;
    uint8_t  viewport_x;
} docset_t;

#define DOC_8BIT 0
#define DOC_UTF8 1
static docset_t g_doc[2];                 /* built=0 для обоих: _DATA зануляется в crt0 gsinit
                                           * (с 2026-07-04), явный {0}-инициализатор больше
                                           * не нужен — он стоил ~260 байт _INITIALIZER */
static uint8_t  g_active_doc = DOC_8BIT;  /* какой набор сейчас в живых глобалах */
static uint8_t  g_disp_doc   = DOC_8BIT;  /* какой набор показан пользователю */

static void doc_switch(uint8_t target);   /* forward (нужна в progress_tick) */

/* Статус-бар, меню и спиннер — в mdview2_status.c. */

/* ==================================================================
 *   Примитивы экрана, доступ к EMM/окну W3 (fb/win_rest), палитра
 * ================================================================== */

/* Печатает строку с указанным атрибутом, начиная с позиции (x,y),
 * и останавливается на конце строки или правой границе экрана.
 * `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его
 * можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */
void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr)
{
    uint8_t len = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0;
    len = min(len, strlen(s));
    if (len == 0) return;
    bios_set_place(y, x);
    bios_writeattr_until(s, len, attr, 0);
}


/* Полностью очищает указанную строку экрана символами пробела с attr. */
void fill_row(uint8_t y, uint8_t attr)
{
    bios_clearwin(y, 0, 1, SCREEN_W, attr);
}

/* Процент 0..100 = num*100/den на чистой 16-битной арифметике — чтобы НЕ
 * тянуть 32-битное деление __divulong (~760 Б) ради показа процента в статусе.
 * Оба вызова (MD/RAW) дают операнды, влезающие в 16 бит (≤18432 и ≤1024),
 * переполняет только *100 — поэтому масштабируем оба вниз, пока num*100 не
 * влезёт в uint16, затем считаем долю циклом-вычитанием (без div-хелпера).
 * Инвариант вызовов: num ≤ den; результат всё равно ограничен 100. */
uint8_t pct16(uint16_t num, uint16_t den)
{
    if (den == 0) return 0;
    while (num > 655) { num >>= 1; den >>= 1; }   /* num*100 ≤ 65500 < 65536 */
    if (den == 0) return 100;                      /* страховка от num>den */
    uint16_t n = (uint16_t)(num * 100u);
    uint8_t  p = 0;
    while (n >= den && p < 100) { n = (uint16_t)(n - den); p++; }
    return p;
}


/* Отображает указанную EMM-страницу файла в окно W3.
 * Если эта страница уже активна, переключение не выполняется. */
// static inline void map_page(uint8_t page)
// {
//     if (page != cur_page) {
//         /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */
//         sprinter_page_w3(file_phys[page]);
//         cur_page = page;
//     }
// }


/* Читает один байт логического файла по смещению p.
 * При переходе между страницами автоматически переключает W3.
 * Проверка границ выполняется вызывающей стороной.
 *
 * Эквивалент C-кода (оставлен для справки):
 *     uint8_t *pb   = (uint8_t *)&p;
 *     uint8_t  page = (pb[1] >> 6) | (pb[2] << 2);
 *     uint16_t off  = ((pb[1] & 0x3F) << 8) | pb[0];
 *     map_page(page);              // if (page!=cur_page){cur_page=page; OUT W3,file_phys[page];}
 *     return FILE_BUF[off];        // *(0xC000 + off)
 *
 * Ручной asm — это горячая примитивная операция (вызывается сотни тысяч раз
 * при индексации 50КБ-файла); C-версия тратила вызов функции + 32-битную
 * арифметику на КАЖДЫЙ байт. Раскладка __sdcccall(1): аргумент uint32_t в
 * HLDE (E=pb[0], D=pb[1], L=pb[2], H=pb[3] — проверено sdcc -S), возврат
 * char в A. Координация маппинга с bank_read/bank_write держится на том,
 * что _io_page_w3 — это ПОРТ (__sfr 0xE2): здесь пишем его через OUT,
 * bank_read читает через IN — общий аппаратный регистр, без теневой RAM.
 * NB: __naked обязателен (внутри собственный ret); IX/IY не трогаем
 * (callee-saved), BC/DE/HL/AF — caller-saved, можно клобберить. */
char fb(uint32_t p) __naked
{
    (void)p;   /* p = HLDE; доступ только из asm */
    __asm
        ; off -> BC = 0xC000 + ((pb[1]&0x3F)<<8 | pb[0]) ; D=pb[1], E=pb[0]
        ld    a, d
        or    a, #0xC0          ; (D|0xC0) == (D&0x3F)|0xC0 — старший байт окна W3
        ld    b, a
        ld    c, e              ; C = pb[0]

        ; page = (pb[1]>>6) | (pb[2]<<2) ; L=pb[2]
        ld    a, l
        add   a, a
        add   a, a              ; A = pb[2]<<2
        ld    e, a              ; E свободен (pb[0] уже сохранён в C)

        ld    a, d
        rlca
        rlca
        and   a, #0x03          ; A = pb[1]>>6
        or    a, e              ; A = page
        ld    e, a              ; E = page

        ; map_page(page)
        ld    a, (_cur_page)
        cp    e
        jr    Z, fb_same
        ld    a, e
        ld    (_cur_page), a
        ld    hl, #_file_phys
        ld    d, #0x00
        add   hl, de            ; hl = &file_phys[page]
        ld    a, (hl)
        out   (#0xE2), a        ; OUT PORT_PAGE_W3 (== _io_page_w3 sfr)
    fb_same:
        ld    a, (bc)           ; A = *(0xC000 + off)
        ret
    __endasm;
}

/* ESTEX WINREST (5Ah, RST 0x10) — копирует готовый буфер (char,attr)
 * из смещения `off` ВНУТРИ EMM-страницы `page` прямо в текстовое окно
 * экрана, минуя любые BIOS print-вызовы. Формат буфера подтверждён
 * эмпирически в MAME (см. memory/sprinter_winrest_format.md и
 * tests/winrest/winrest.c): пары (char,attr) по ячейкам, по строкам,
 * шаг строки = width*2, без паддинга. row/col — левый верхний угол
 * вывода на экране, h/w — высота/ширина в символах.
 *
 * Регистровая раскладка SDCC __sdcccall(1) для (row,col,h,w,page,off)
 * проверена через `sdcc -S` (пробный non-naked враппер): A=row, L=col,
 * стек [SP+2]=h [SP+3]=w [SP+4]=page [SP+5..6]=off (little-endian).
 *
 * История: до 2026-06-23 `off` не было — IX всегда указывал на самое
 * начало страницы (#0xC000), это работало только пока в кэше было
 * РОВНО ОДНА запись на странице со смещением 0 (Фаза 1 self-test).
 * Когда Фаза 2 начала пакетировать много строк на одной странице с
 * РАЗНЫМИ off, всё стало читать с начала страницы (т.е. контент первой
 * записи) независимо от реальной строки — добавлен off, IX = 0xC000+off. */
void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off) __naked
{
    (void)row; (void)col; (void)h; (void)w; (void)page; (void)off;
    __asm
        ld    iy, #2
        add   iy, sp
        ld    d, a              ; D = row
        ld    e, l              ; E = col
        push  ix                ; сохранить IX вызывающей функции (callee-saved)
        ld    l, 3 (iy)         ; off (младший байт), SP+5
        ld    h, 4 (iy)         ; off (старший байт), SP+6
        ld    bc, #0xC000
        add   hl, bc
        push  hl
        pop   ix                ; IX = 0xC000 + off
        ld    h, 0 (iy)         ; H = height
        ld    a, 1 (iy)         ; width -> L
        ld    l, a
        ld    a, 2 (iy)         ; page -> B
        ld    b, a
        ld    c, #0x5A          ; ESTEX WINREST
        di
        rst   #0x10
        ei
        pop   ix                ; восстановить IX вызывающей функции
        pop   hl                ; адрес возврата
        inc   sp
        inc   sp
        inc   sp
        inc   sp
        inc   sp                ; снять h,w,page,off (1+1+1+2 байт) со стека
        jp    (hl)
    __endasm;
}

/* Char-хелперы, idx_get/idx_put/seg_off, set_*_cur — в mdview2_index.c. */

/* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */
static void set_pallete(void) {
    uint8_t buff[64];
    for(int i = 0; i < 16; i++) {
        for(int j = 0; j < 16; j++) {
            buff[j * 4] = md_pallete[i * 4];
            buff[j * 4 + 1] = md_pallete[i * 4 + 1];
            buff[j * 4 + 2] = md_pallete[i * 4 + 2];
            buff[j * 4 + 3] = md_pallete[i * 4 + 3];
        }

        text_pal_load(TEXT_PAL_INK,         i * 16, 16, md_pallete);
        text_pal_load(TEXT_PAL_BLINK_INK,   i * 16, 16, md_pallete);
        text_pal_load(TEXT_PAL_PAPER,       i * 16, 16, buff);
        text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16, 8, buff + 32);   
        text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16 + 8, 8, buff + 32);   
    }
}

/* ==================================================================
 *                    Загрузка/выгрузка файла
 * ================================================================== */

/* Загружает исходные байты файла в EMM-страницы (без индекса/кэша — те
 * выделяются отдельно на КАЖДЫЙ набор через alloc_set_storage()).
 * Возврат: 0 — успех, отрицательное значение — код ошибки. */
static int load_file(const char *path)
{
    int fd = open(path, O_RDONLY);
    if (fd < 0) 
        return -1;

    long sz = lseek(fd, 0, SEEK_END);
    if (sz < 0) {
        close(fd);
        return -2;
    }
    /* Файл больше MAX_FILE (256 КБ) не отвергаем, а КЛАМПим: читаем первые
     * 256 КБ, индексатор допишет строку-предупреждение (TRUNC_FILE). */
    g_file_clamped = 0;
    if ((uint32_t)sz > MAX_FILE) {
        sz = (long)MAX_FILE;
        g_file_clamped = 1;
    }

    file_size = (uint32_t)sz;
    lseek(fd, 0, SEEK_SET);

    /* Выделяем ровно столько EMM-страниц, сколько нужно под файл. */
    file_pages = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE);
    if (file_pages == 0) file_pages = 1;
    file_blk = mem_alloc_pages(file_pages);
    if (file_blk == 0) {
        close(fd);
        return -3;
    }

    /* Читаем файл кусками по 16 КБ, последовательно маппя страницы в W3.
     * На последней итерации читаем остаток меньше PAGE_SIZE. */
    uint32_t remaining = file_size;
    for (uint8_t page = 0; page < file_pages; page++) {
        spinner_tick();
        uint8_t phys = mem_get_page(file_blk, page);
        file_phys[page] = phys;
        sprinter_page_w3(phys);
        cur_page = page;
        uint16_t chunk = (remaining > PAGE_SIZE)
                       ? (uint16_t)PAGE_SIZE
                       : (uint16_t)remaining;
        int n = read(fd, FILE_BUF, chunk);
        if (n != (int)chunk) {
            close(fd);
            return -4;
        }
        remaining -= chunk;
    }
    close(fd);
    return 0;
}

/* Выделяет в живые глобалы блок индекса + директорию рендер-кэша под текущий
 * набор. file_pages должно быть уже выставлено (размер набора). Сбрасывает
 * пул контента в пустое состояние (страницы доращиваются лениво при рендере).
 * Возврат: 0 — успех, -5/-6 — нехватка EMM. */
static int alloc_set_storage(void)
{
    uint8_t want;

    want = (uint8_t)((file_pages >> 1) + 1u);
    if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES;
    index_blk = mem_alloc_pages(want);
    if (index_blk == 0) return -5;
    index_pages = want;
    for (uint8_t i = 0; i < index_pages; i++)
        index_phys[i] = mem_get_page(index_blk, i);
    max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE;

    want = (uint8_t)((file_pages >> 1) + 1u);
    if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES;
    cache_dir_blk = mem_alloc_pages(want);
    if (cache_dir_blk == 0) return -6;
    cache_dir_pages = want;
    for (uint8_t i = 0; i < cache_dir_pages; i++)
        cache_dir_phys[i] = mem_get_page(cache_dir_blk, i);

    cache_content_n_pages = 0;
    cache_cur_page_idx    = 0;
    cache_cur_off         = 0;
    return 0;
}

static void doc_save_live(uint8_t i);   /* forward */

/* Освобождает все EMM-блоки обоих наборов (индекс/директория/пул контента)
 * плюс два файловых буфера (исходный 8-битный и UTF-8). */
static void unload_file(void)
{
    doc_save_live(g_active_doc);         /* зафиксировать живой набор в g_doc */
    for (uint8_t s = 0; s < 2; s++) {
        docset_t *d = &g_doc[s];
        if (d->index_blk)     { mem_free_block(d->index_blk);     d->index_blk = 0; }
        if (d->cache_dir_blk) { mem_free_block(d->cache_dir_blk); d->cache_dir_blk = 0; }
        for (uint8_t i = 0; i < d->cache_content_n_pages; i++)
            if (d->cache_content_blk[i]) mem_free_block(d->cache_content_blk[i]);
        d->cache_content_n_pages = 0;
    }
    if (orig_file_blk) { mem_free_block(orig_file_blk); orig_file_blk = 0; }
    if (utf_blk)       { mem_free_block(utf_blk);       utf_blk = 0; }
    file_blk = 0;
}

/* Парсер/индексатор (classify_line, scan_join_stream, inline_scan, index_lines, emph_to_attr) — в mdview2_index.c. */


/* Таблицы ремапа, set_encoding, кодировки и UTF-8 конвертер — в mdview2_enc.c. */

/* ==================================================================
 *           Наборы документа (doc-slots): сохранение/свап
 * ==================================================================
 * Два сосуществующих набора (DOC_8BIT / DOC_UTF8) живут как копии всего
 * состояния (file/index/cache/позиция) в g_doc[]; «живые» глобалы — это
 * активный набор. doc_save_live: живые → g_doc[i]; doc_load_live: g_doc[i]
 * → живые (+ инвалидация маппинга fb() и активной кодировки). */
static void doc_save_live(uint8_t i)
{
    docset_t *d = &g_doc[i];
    d->enc = g_encoding;
    d->file_blk = file_blk; d->file_pages = file_pages; d->file_size = file_size;
    memcpy(d->file_phys, file_phys, MAX_PAGES);
    d->index_blk = index_blk; d->index_pages = index_pages; d->index_truncated = index_truncated;
    d->n_lines = n_lines; d->max_lines = max_lines;
    memcpy(d->index_phys, index_phys, MAX_INDEX_PAGES);
    d->cache_dir_blk = cache_dir_blk; d->cache_dir_pages = cache_dir_pages;
    memcpy(d->cache_dir_phys, cache_dir_phys, MAX_CACHE_DIR_PAGES);
    d->cache_content_n_pages = cache_content_n_pages;
    d->cache_cur_page_idx = cache_cur_page_idx; d->cache_cur_off = cache_cur_off;
    memcpy(d->cache_content_blk,  cache_content_blk,  MAX_CACHE_CONTENT_PAGES);
    memcpy(d->cache_content_phys, cache_content_phys, MAX_CACHE_CONTENT_PAGES);
    d->top_line = top_line; d->viewport_x = viewport_x;
}

static void doc_load_live(uint8_t i)
{
    docset_t *d = &g_doc[i];
    file_blk = d->file_blk; file_pages = d->file_pages; file_size = d->file_size;
    memcpy(file_phys, d->file_phys, MAX_PAGES);
    index_blk = d->index_blk; index_pages = d->index_pages; index_truncated = d->index_truncated;
    n_lines = d->n_lines; max_lines = d->max_lines;
    memcpy(index_phys, d->index_phys, MAX_INDEX_PAGES);
    cache_dir_blk = d->cache_dir_blk; cache_dir_pages = d->cache_dir_pages;
    memcpy(cache_dir_phys, d->cache_dir_phys, MAX_CACHE_DIR_PAGES);
    cache_content_n_pages = d->cache_content_n_pages;
    cache_cur_page_idx = d->cache_cur_page_idx; cache_cur_off = d->cache_cur_off;
    memcpy(cache_content_blk,  d->cache_content_blk,  MAX_CACHE_CONTENT_PAGES);
    memcpy(cache_content_phys, d->cache_content_phys, MAX_CACHE_CONTENT_PAGES);
    top_line = d->top_line; viewport_x = d->viewport_x;
    cur_page = 0xFF;                   /* следующий fb() пере-смаппит страницу */
    set_encoding(d->enc);
}

static void doc_switch(uint8_t target)
{
    if (target == g_active_doc) 
        return;
    doc_save_live(g_active_doc);
    doc_load_live(target);
    g_active_doc = target;
}

/* Автоопределение/имя кодировки, ремап-вывод и UTF-8→CP866 конвертер — в mdview2_enc.c. */

/* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_md.c. */



/* ==================================================================
 *           Прогрессивная загрузка и диспетчер клавиш (MD)
 * ==================================================================
 * index_lines() строит индекс кооперативно и периодически зовёт
 * progress_tick(): тот рисует первый готовый экран, крутит спиннер и
 * обрабатывает клавиши на лету (load_key). Диалог справки (F1) — в
 * mdview2_help.c, RAW-навигация — в mdview2_raw.c. */

/* Обработка клавиши во время прогрессивной загрузки: скролл строго в
 * пределах готовых строк (drawable_lines); End = последняя готовая страница;
 * F1 = справка (show_help сам блокирует и перерисовывает). */
static void load_key(uint16_t k)
{
    uint8_t  ascii  = (uint8_t)(k & 0xFF);
    uint8_t  scan   = (uint8_t)((k >> 8) & 0x7F);
    uint16_t dr     = drawable_lines();
    uint16_t maxtop = (dr > VIEW_H) ? (uint16_t)(dr - VIEW_H) : 0;
    uint16_t nt     = top_line;

    if (ascii == 0x1B || scan == KEY_F10) { 
        g_abort = 1; 
        return; 
    }  /* Esc/F10 → выход */
    if (ascii) 
        return;                                              /* прочие ascii игнорируем */

    switch (scan) {
        case KEY_UP:    
            nt = (top_line >= 1) ? (uint16_t)(top_line - 1) : 0; 
            break;
        case KEY_DOWN:  
            nt = top_line + 1; 
            break;
        case KEY_PGUP:  
            nt = (top_line >= VIEW_H) ? (uint16_t)(top_line - VIEW_H) : 0; 
            break;
        case KEY_PGDN:  
            nt = top_line + VIEW_H; 
            break;
        case KEY_HOME:  
            top_line = 0;      
            viewport_x = 0; 
            draw_viewport_from_cache(); 
            render_md_status_numbers(); 
            return;
        case KEY_END:   
            top_line = maxtop; 
            viewport_x = 0; 
            draw_viewport_from_cache(); 
            render_md_status_numbers(); 
            return;
        case KEY_LEFT:  
            md_scroll_horizon(-(int8_t)HPAN_STEP); 
            render_md_status_numbers(); 
            return;
        case KEY_RIGHT: 
            md_scroll_horizon(+(int8_t)HPAN_STEP); 
            render_md_status_numbers(); 
            return;
        case KEY_F1:    
            show_help(); 
            return;
        case KEY_F8:    
            /* во время сборки крутим ТОЛЬКО 8-бит и только если набор 8-битный
             * (ремап на отрисовке; индексация не зависит от кодировки) */
            if (g_f8_enabled && g_active_doc == DOC_8BIT) {
                uint8_t nx = (g_encoding == ENC_CP866)  ? ENC_CP1251 :
                                (g_encoding == ENC_CP1251) ? ENC_KOI8R  : ENC_CP866;
                set_encoding(nx); 
                g_doc[DOC_8BIT].enc = nx;
                draw_viewport_from_cache();
                render_full_status();
            }
            return;
        default:
            return;
    }

    if (nt > maxtop) 
        nt = maxtop;
    if (nt != top_line) {
        top_line = nt;
        draw_viewport_from_cache();
        render_md_status_numbers();
    }
}

/* Кооперативный «тик» прогрессивной загрузки — вызывается из index_lines(). */
void progress_tick(void)
{
#if WITH_RAW
    /* В RAW-режиме буфер документа готов сразу после конвертации (до индекса),
     * поэтому первый экран RAW можно показать немедленно — рисуем raw_draw, а
     * не MD-вид (иначе мелькал бы MD). Индекс при этом строится дальше (нужен
     * для MD-вида). */
    if (g_view != VIEW_MD) {
#if WITH_HEX
        if (g_view == VIEW_HEX) {       /* HEX показывает оригинал — экран уже актуален */
            spinner_tick();
            return;
        }
#endif
        if (!g_first_drawn) {
            raw_draw();
            // render_raw_status();
            g_first_drawn = 1;
            cur_page = 0xFF;            /* raw_draw сбил W3-маппинг, на который опирается fb() */
        }
        spinner_tick();
        return;
    }
#endif
    /* Прогрессивная отрисовка строящегося (всегда показываемого) набора. */
    uint8_t touched = 0;
    if (!g_first_drawn && drawable_lines() >= VIEW_H) {
        draw_viewport_from_cache();
        render_md_status_numbers();
        g_first_drawn = 1;
        touched = 1;
    }
    if (n_lines >= (uint16_t)(g_last_prog + 10)) {
        g_last_prog = n_lines;
        spinner_tick();
        if (g_first_drawn) 
            render_md_status_numbers();
        touched = 1;
    }
    if (g_first_drawn && kbhit()) {
        load_key(getkey());
        touched = 1;
    }
    /* Отрисовка/WINREST/BIOS могли сбить маппинг W3, на который опирается fb()
     * при индексации → форсируем ре-маппинг страницы файла на следующем fb(). */
    if (touched) 
        cur_page = 0xFF;
}

/* ==================================================================
 *                            Точка входа
 * ================================================================== */

/* Строит набор `slot` кооперативно (index_step + progress_tick).
 * visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана
 * и скролл по готовым строкам. visible=0 — вторичный: индексация идёт в живых
 * глобалах, а progress_tick через свап показывает/листает уже готовый
 * g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7).
 *
 * Если slot прямо сейчас строится ФОНОМ (g_bg_doc, см. bg_step ниже) —
 * не начинаем заново, а ДОКРУЧИВАЕМ: загружаем частично построенный набор
 * и продолжаем с сохранённой позиции индексатора (его межшаговое состояние
 * живёт в статиках модуля index и переживает свапы наборов).
 *
 * UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем
 * живой буфер на utf и запускаем конвертацию первого чанка; дальше index_step
 * докручивает её впереди позиции чтения (g_utf_building). Конвертер читает
 * оригинал из orig_file_phys[] через cv_read (не через fb, который теперь
 * указывает на utf). */
/* Настраивает живые глобалы под сборку набора slot: файловый буфер (utf/orig),
 * индекс + директория кэша, сброс индексатора. Общая часть build_doc и
 * bg_build_start. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). */
static int doc_setup(uint8_t slot)
{
    if (slot == DOC_UTF8) {
        if (!utf_avail) {                       /* первый вход: выделить страницы */
            if (!utf_alloc())
                return -7;   /* выделяет страницы + сбрасывает конвертер */
            utf_avail = 1;
        }
        file_blk = utf_blk;
        file_pages = utf_pages;
        memcpy(file_phys, utf_phys, MAX_PAGES);
        if (g_utf_building) {
            file_size = 0;
            cur_page  = 0xFF;
            utf_convert_more(CONV_MARGIN);      /* первый чанк — для первого экрана */
        } else {
            file_size = utf_size;
        }
    } else {
        file_blk = orig_file_blk;
        file_pages = orig_file_pages;
        file_size = orig_file_size;
        memcpy(file_phys, orig_file_phys, MAX_PAGES);
    }
    cur_page = 0xFF;

    int rc = alloc_set_storage();
    if (rc < 0)
        return rc;
    index_begin();
    return 0;
}

static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
{
    if (g_bg_doc == slot) {                     /* фон уже строит его — продолжить */
        doc_load_live(slot);
        g_bg_doc = 0xFF;
    } else {
        int rc = doc_setup(slot);
        if (rc < 0)
            return rc;
    }

    set_encoding(enc);
    top_line = 0;
    viewport_x = 0;
    g_active_doc = slot;
    if (visible) {
        g_disp_doc = slot;
        g_first_drawn = 0;
        status_encoding();  /* показать кодировку сразу (детект/F8), не дожидаясь индексации */
        cur_page = 0xFF;    /* BIOS-вывод мог сбить W3-маппинг, на который опирается fb() */
    }

    g_loading   = 1;
    g_last_prog = 0;
    while (!index_step(255)) {}
    g_loading   = 0;

    /* Зафиксировать конвертацию (на случай обрыва индекса по max_lines до конца
     * исходника) — иначе g_utf_building остался бы 1 и сломал бы следующую сборку. */
    if (g_utf_building) { 
        utf_size = file_size; 
        g_utf_building = 0; 
    }

    g_doc[slot].built = 1;
    doc_save_live(slot);
    return 0;
}

/* ==================================================================
 *      Фоновая сборка второго набора (idle-time, между клавишами)
 * ==================================================================
 * Пока пользователь читает документ, главный цикл в паузах между клавишами
 * докручивает индексацию второго набора (UTF-8 при 8-битном первичном и
 * наоборот) порциями index_step: свап на bg-набор → несколько итераций →
 * свап обратно. Экран при этом не трогается (g_bg_building гасит спиннер),
 * задержка реакции на клавишу — не больше одного шага. По готовности F8
 * переключает наборы мгновенно; F8 ДО готовности докручивает сборку видимо
 * (ветка resume в build_doc), не начиная её заново. */

#define BG_STEP_BUDGET   2u    /* итераций индексатора на idle-шаг: латентность клавиши
                                * = длительность шага (~единицы мс на обычном абзаце) */
#define BG_KEY_HOLDOFF   300u  /* пустых опросов kbhit после клавиши до возобновления
                                * фона — при автоповторе (скролл) фон не вклинивается */

/* Готовит фоновую сборку второго набора: настраивает живые глобалы (буфер,
 * индекс, кэш, index_begin) так же, как build_doc, но БЕЗ индексации, и
 * возвращает живым прежний набор. При нехватке EMM фон просто не стартует —
 * остаётся сегодняшний ленивый путь по F8. */
static void bg_build_start(void)
{
    uint8_t slot = (uint8_t)((g_active_doc == DOC_8BIT) ? DOC_UTF8 : DOC_8BIT);
    uint8_t vis  = g_active_doc;
    if (g_doc[slot].built || (slot == DOC_UTF8 && g_utf_failed))
        return;                                 /* строить нечего */

    doc_save_live(vis);
    if (doc_setup(slot) < 0) {                  /* нет EMM — как и в ленивом пути */
        if (slot == DOC_UTF8) g_utf_failed = 1;
        doc_load_live(vis);
        return;
    }
    top_line = 0;
    viewport_x = 0;

    g_active_doc = slot;                        /* живые глобалы = bg-набор... */
    doc_switch(vis);                            /* ...сохранить его и вернуть видимый */
    g_bg_doc = slot;
}

/* Один idle-шаг фона: свап на строящийся набор, порция индексации, свап
 * обратно. Межшаговое состояние индексатора/UTF-конвертера — статики их
 * модулей, обработчики клавиш видимого набора их не трогают. По завершении
 * финализирует набор (дальше F8 переключается мгновенно). */
static void bg_step(void)
{
    uint8_t vis = g_active_doc;
    doc_switch(g_bg_doc);
    g_bg_building = 1;
    uint8_t done = index_step(BG_STEP_BUDGET);
    g_bg_building = 0;
    if (done) {
        if (g_utf_building) {                   /* как build_doc: обрыв индекса до конца исходника */
            utf_size = file_size;
            g_utf_building = 0;
        }
        g_doc[g_bg_doc].built = 1;              /* doc_save_live поле built не трогает */
        g_bg_doc = 0xFF;
    }
    doc_switch(vis);
}

/* Пропорциональный перенос байт-позиции между буферами разного размера
 * (8bit ↔ UTF-8 при F8): off * to / from БЕЗ __mullong/__divulong. Один
 * цикл restoring-деления: бит k дроби off/from (сравнение с from/2^k)
 * сразу добавляет to/2^k в результат — сумма to/2^k по установленным
 * битам и есть off*to/from. Точность from/65536 (≤4 байта на максимальном
 * файле); ляжет на границу строки через line_at_off / raw_reanchor. */
static uint32_t map_off(uint32_t off, uint32_t from, uint32_t to)
{
    if (from == to || from == 0) return off;
    uint32_t res = 0;
    for (uint8_t i = 0; i < 16; i++) {
        from >>= 1;
        to   >>= 1;
        if (off >= from) { off -= from; res += to; }
    }
    return res;
}

/* Байт-позиция текущего вида (верх экрана) — единая «валюта» для переноса
 * позиции между видами/наборами. Валюта = offset АКТИВНОГО буфера; HEX живёт
 * в байтах оригинала, для UTF-набора конвертируем через map_off. */
static uint32_t view_pos(void)
{
#if WITH_HEX
    if (g_view == VIEW_HEX)
        return (g_active_doc == DOC_UTF8)
             ? map_off(hex_pos(), orig_file_size, file_size)
             : hex_pos();
#endif
#if WITH_RAW
    if (g_view != VIEW_MD) return raw_pos();
#endif
    return seg_off(top_line);
}

/* Поставить текущий вид на байт-позицию off (активного буфера). */
static void view_reanchor(uint32_t off)
{
#if WITH_HEX
    if (g_view == VIEW_HEX) {
        hex_reanchor((g_active_doc == DOC_UTF8)
                     ? map_off(off, file_size, orig_file_size)
                     : off);
        return;
    }
#endif
#if WITH_RAW
    if (g_view != VIEW_MD) { raw_reanchor(off); return; }
#endif
    top_line = line_at_off(off);
}

/* Переключает кодировку на `next` (вызывается из главного цикла по F8).
 * 8-бит↔8-бит внутри одного набора — мгновенный ремап; переход в другой набор
 * — мгновенный doc_switch, если он построен, иначе ЛЕНИВАЯ сборка по месту
 * (со спиннером), результат кэшируется. */
/* Меняет кодировку: тот же набор — ремап; другой готовый — doc_switch;
 * непостроенный — ЛЕНИВАЯ ТИХАЯ сборка (спиннер, без отрисовки документа).
 * НЕ перерисовывает документ/статус — это делает вызывающий в своём режиме
 * (MD: draw_viewport_from_cache; RAW: raw_draw), чтобы не мелькал чужой вид. */
static void switch_encoding(uint8_t next)
{
    uint8_t target = (next == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT;

    if (target == g_active_doc) {                   /* тот же набор — только ремап */
        set_encoding(next); 
        g_doc[target].enc = next;
    } else if (g_doc[target].built) {               /* набор готов — мгновенный свап */
        /* Позицию переносим пропорцией байтов (буферы наборов разного
         * размера), а не из сохранённой в g_doc копии — иначе каждый набор
         * скачет по своей старой позиции. */
        uint32_t old_off  = view_pos();
        uint32_t old_size = file_size;
        doc_switch(target);
        g_disp_doc = target;
        set_encoding(next);
        g_doc[target].enc = next;
        view_reanchor(map_off(old_off, old_size, file_size));
    } else {                                        /* строим набор лениво */
        doc_save_live(g_active_doc);                /* сохранить текущий (позиция и т.п.) */
#if WITH_RAW
        uint32_t raw_save = raw_pos();
        if (VIEW_IS_RAW(g_view))                    /* HEX показывает оригинал — его позиция инвариантна */
            raw_reanchor(0);                        /* прогрессивный показ нового буфера — с начала */
        g_ready = 0;
        render_menu();                              /* идёт индексация — прячем F2 */
#endif
        spinner_show(1);
        int rc = build_doc(target, next, 1);        /* visible: прогрессивный показ первого экрана
                                                     * (progress_tick рисует по текущему виду — MD/RAW) */
        spinner_show(0);
#if WITH_RAW
        g_ready = 1; 
        render_menu();
#endif
        if (rc < 0) {                               /* нет EMM — откат на текущий набор */
            if (target == DOC_UTF8)
                g_utf_failed = 1;
            doc_load_live(g_active_doc);
#if WITH_RAW
            if (VIEW_IS_RAW(g_view))
                raw_reanchor(raw_save);             /* буфер прежний — вернуть позицию RAW */
#endif
            return;
        }
        g_disp_doc = target;                        /* build_doc уже выставил active/enc */
    }
    clamp_top();                                    /* привести top_line к новому набору (для MD) */
}

/* ==================================================================
 *                         Вывод ошибки
 * ================================================================== */

static void on_exit(void) { 
    unload_file();
    pal_reset(PAL_CGA);
}

int main(int argc, char **argv)
{
    const char *path;
    if (argc >= 2) {
        path = argv[1];
    } else {
        path = "README.MD";
        /* Дефолтный файл отсутствует — выводим подсказку без смены режима экрана. */
        {
            int chk = open(path, O_RDONLY);
            if (chk < 0) {
                // settextmode(TEXT_MODE_80x32);
                clrscr_attr(ATTR_TEXT);
                cputs("MDView v0.3  --  Markdown Viewer for Sprinter\r\n" \
                    "(c) 2026 Petrov A.G.\r\n\r\n" \
                    "Usage:  mdview2 <file.md>\r\n" \
                    "        Displays a Markdown document (CP866/CP1251/KOI8R/UTF8, up to 128 KB).\r\n");
                exit(-1);
            }
            close(chk);
        }
    }

    atexit(on_exit);

    /* Копируем путь в static filename[] для отображения в статус-баре. */
    {
        uint8_t i = 0;
        while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) {
            filename[i] = path[i];
            i++;
        }
        filename[i] = 0;
    }

    /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер
     * уже во время загрузки файла и построения индекса. */
    settextmode(TEXT_MODE_80x32);
    clrscr_attr(ATTR_TEXT);
    set_pallete();
    prerender_status();
    render_menu();
    render_full_status();
    render_percent_progress(0);
    spinner_show(1);

    int rc = load_file(path);
    if (rc < 0) {
        spinner_show(0);
        clrscr_attr(ATTR_RESET);
        textattr(ATTR_RESET);
        cputs("MDView: cannot load file '");
        cputs(path);
        cputs("'\n\rError: ");
        switch (rc) {
            case -1: cputs("open failed");                break;
            case -2: cputs("seek failed");                break;
            case -3: cputs("mem_alloc_pages failed");     break;
            case -4: cputs("short read");                 break;
            case -5: cputs("index alloc failed");         break;
            case -6: cputs("render-cache dir alloc failed"); break;
        }
        cputs("\n\r\n\r");
        exit(-1);
    }
    /* Scratch-страница под ремап-буфер строки (win_rest_remap). */
    g_scratch_phys = mem_get_page(mem_alloc_pages(1), 0);

    /* Оба набора пока не построены (страховка к инициализатору g_doc). */
    g_doc[DOC_8BIT].built = 0;
    g_doc[DOC_UTF8].built = 0;

    /* Снимок исходного 8-битного буфера (его читает набор DOC_8BIT). */
    orig_file_blk = file_blk; orig_file_pages = file_pages; orig_file_size = file_size;
    memcpy(orig_file_phys, file_phys, MAX_PAGES);

    /* Детекция кодировки по оригиналу. UTF-8 конвертацию НЕ делаем здесь —
     * она дорогая (полный проход) и нужна только для UTF-8 набора, который
     * строится через build_doc лениво. Так первый экран рисуется по индексации
     * первичного набора без лишнего прохода. */
    uint8_t det = detect_encoding();

    /* Первичный (показываемый) набор — по детекции; второй строится ЛЕНИВО по
     * первому F8 (switch_encoding). Во время сборки 8-битного первичного F8
     * разрешён (ремап 8-бит, см. load_key) → показываем метку F8; для UTF-8
     * первичного переключаться пока некуда → метку прячем. */
    uint8_t prim_enc = det;
    uint8_t prim_doc = (prim_enc == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT;
    g_f8_enabled = (prim_doc == DOC_8BIT);
    render_menu();

    rc = build_doc(prim_doc, prim_enc, 1);   /* первичный — видимый */
    if (rc < 0 && prim_doc == DOC_UTF8) {    /* UTF-8 не вышел (нет EMM) → 8-бит */
        prim_doc = DOC_8BIT; prim_enc = ENC_CP866;
        rc = build_doc(DOC_8BIT, ENC_CP866, 1);
    }
    if (rc < 0) {
        spinner_show(0);
        clrscr_attr(ATTR_RESET);
        textattr(ATTR_RESET);
        cputs("MDView: index/cache alloc failed\n\r\n\r");
        exit(-1);
    }
    if (g_abort) 
        exit(0);       /* F10/Esc во время загрузки — корректный выход */
    if (n_lines == 0) {
        clrscr_attr(ATTR_RESET);
        textattr(ATTR_RESET);
        cputs("mdview: empty file");
        exit(-1);
    }

    /* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся
     * лениво по первому переключению) и рисуем финальный статус с ПОЛНЫМ
     * счётчиком строк. */
    spinner_show(0);
    g_f8_enabled = 1;
#if WITH_RAW
    g_ready      = 1;     /* документ построен — теперь доступен F2 (RAW) */
#endif
    render_menu();
    clamp_top();
    render_full_status();
    draw_viewport_from_cache();

    /* Второй набор (UTF-8 при 8-битном первичном и наоборот) готовим фоном
     * в паузах между клавишами — к первому F8 он обычно уже построен. */
    bg_build_start();

    for (;;) {
        /* Idle-фон: пока второй набор не достроен, вместо блокирующего getkey
         * опрашиваем клавиатуру и тратим паузы на bg_step. Холдаун после
         * каждой клавиши: при активном скролле (автоповтор) фон молчит и
         * плавность не страдает. После достройки — снова блокирующий getkey. */
        if (g_bg_doc != 0xFF) {
            uint16_t idle = 0;
            while (!kbhit()) {
                if (idle < BG_KEY_HOLDOFF) {
                    idle++;
                    continue;
                }
                bg_step();
                if (g_bg_doc == 0xFF)
                    break;
            }
        }
        uint16_t k = getkey();
        uint8_t  ascii = (uint8_t)(k & 0xFF);
        uint8_t  scan  = (uint8_t)((k >> 8) & 0x7F);

        if (ascii) {
            if (ascii == 0x1B) 
                break;       /* клавиша Esc */
            continue;
        }
        if (scan == KEY_F10) {
            break;
        }
        if (scan == KEY_F1)  { 
            show_help(); 
            continue; 
        }   /* show_help сам перерисует */
        /* F2 — тумблер RAW ↔ MD (из HEX тоже уводит в MD).
         * Рабочий только после постройки документа. */
        if (scan == KEY_F2) {
            if (!g_ready) continue;
            if (g_view == VIEW_MD) {
                g_view = g_raw_last;                 /* → RAW (последний под-режим) */
                raw_seed_from(top_line);             /* позиция из MD */
                raw_screen_init();
                status_encoding();
                render_menu();
                render_raw_status();
                raw_draw();
            } else {
                uint32_t off = view_pos();           /* позиция из RAW/HEX */
                if (VIEW_IS_RAW(g_view))
                    g_raw_last = g_view;             /* запомнить под-режим RAW */
                g_view = VIEW_MD;
                top_line = line_at_off(off);
                clamp_top();
                render_full_status(); render_menu();
                draw_viewport_from_cache();
            }
            continue;
        }
#if WITH_HEX
        /* F4 — тумблер HEX ↔ прежний вид (MD/RAW). */
        if (scan == KEY_F4) {
            if (!g_ready) continue;
            uint32_t off = view_pos();               /* позиция текущего вида */
            if (g_view == VIEW_HEX) {                /* HEX → прежний вид */
                g_view = g_hex_last;
                view_reanchor(off);
                if (g_view == VIEW_MD) {
                    clamp_top();
                    render_full_status(); render_menu();
                    draw_viewport_from_cache();
                } else {
                    raw_screen_init();
                    status_encoding(); render_menu();
                    render_raw_status(); raw_draw();
                }
            } else {                                 /* MD/RAW → HEX */
                g_hex_last = g_view;
                g_view = VIEW_HEX;
                view_reanchor(off);                  /* active → байты оригинала */
                hex_screen_init();
                status_encoding(); render_menu();
                render_hex_status(); hex_draw();
            }
            continue;
        }
#endif
        /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */
        if (scan == KEY_F8) {     
            uint8_t next = (g_encoding == ENC_CP866)  ? ENC_CP1251 :
                        (g_encoding == ENC_CP1251) ? ENC_KOI8R  :
                        (g_encoding == ENC_KOI8R)  ? ENC_UTF8   : ENC_CP866;
            if (next == ENC_UTF8 && g_utf_failed) 
                next = ENC_CP866;  /* UTF-8 недоступен */
            switch_encoding(next);
            if (g_abort)
                exit(0);   /* Esc/F10 во время ленивой досборки */
            /* Перерисовка — по текущему виду (switch_encoding сам не рисует). */
#if WITH_HEX
            if (g_view == VIEW_HEX) {
                status_encoding();
                render_hex_status();
                hex_draw();                /* printable-колонка в новой кодировке */
                continue;
            }
#endif
#if WITH_RAW
            if (g_view != VIEW_MD) {
                status_encoding();
                render_raw_status();
                raw_draw();
                continue;
            }
#endif
            render_full_status();
            draw_viewport_from_cache();
            continue;
        }
        switch(g_view) {
            case VIEW_MD:
                /* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */
                md_key(scan);          /* навигация MD (сама рисует+статус); незнакомые игнорит */
                continue;
#if WITH_HEX
            case VIEW_HEX:
                hex_key(scan);         /* навигация HEX (сама рисует+процент) */
                continue;
#endif
#if WITH_RAW
            case VIEW_RAW_WRAP:
            case VIEW_RAW_HSCROLL:
                /* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */
                /* Wrap ↔ Unwrap */
                if (scan == KEY_F3) {                    
                    g_view = (g_view == VIEW_RAW_WRAP) ? VIEW_RAW_HSCROLL : VIEW_RAW_WRAP;
                    g_raw_last = g_view;
                    raw_renorm();
                    raw_screen_init();
                    render_menu();
                    render_raw_status(); 
                    raw_draw();
                    continue;
                }
                raw_key(scan);
                continue;
#endif /* WITH_RAW */
            default:
                break;
        }
    }
    clrscr_attr(ATTR_TEXT);
    exit(0);
    return 0;
}
