/*
 * mdview2_enc.c — кодировки и конвертация UTF-8 → CP866.
 *
 * Две связанные подсистемы:
 *   1. 8-битные кодировки (CP866/CP1251/KOI8-R) — различаются только ремапом
 *      старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap).
 *      Автоопределение — detect_encoding по частоте ходовых букв.
 *   2. UTF-8 — отдельный набор: исходник инкрементально декодируется в
 *      utf_phys[] как чистый CP866 (utf_convert_more), после чего весь
 *      конвейер индексации/кэша работает по нему без ремапа.
 *
 * Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы);
 * описание разделяемых символов и API — в mdview2.h. Оркестрацией (привязка
 * UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра.
 */

#include <stdint.h>
#include <conio.h>          /* COLOR()/COLOR_* для ATTR_* в attr_is_content */
#include <sprinter.h>       /* sprinter_page_w3 */
#include <sprinter_mem.h>   /* bank_read/bank_write/mem_alloc_pages/mem_get_page */
#include "mdview2.h"

/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */
uint8_t g_encoding = ENC_CP866;        /* активная кодировка */

uint8_t  utf_blk;
uint8_t  utf_pages;
uint8_t  utf_phys[MAX_PAGES];
uint32_t utf_size;
uint8_t  utf_avail;                /* 1 = страницы UTF-8 выделены */
uint8_t  g_utf_building;           /* 1 пока конвертация не дошла до конца исходника */

/* Приватное состояние конвертера (наружу не торчит — сброс через
 * utf_conv_reset(), чтение оригинала через cv_read). */
static uint32_t utf_src;          /* позиция чтения ОРИГИНАЛА (utf-8) */
static uint8_t  cv_page  = 0xFF;       /* orig-страница в W3 для cv_read (своя от cur_page) */

/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80):
 * младшие 128 (ASCII) — identity, в ремапе не участвуют (win_rest_remap
 * трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая
 * пунктуация; неизвестное → '?' (0x3F). Сгенерированы Python codecs. */
static const uint8_t cp1251_to_866[128] = {
    /* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F,
    /* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F,
    /* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4,
    /* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5,
    /* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
    /* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
    /* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
    /* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
};
static const uint8_t koi8r_to_866[128] = {
    /* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE,
    /* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F,
    /* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6,
    /* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63,
    /* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE,
    /* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA,
    /* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E,
    /* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A,
};

/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */
void set_encoding(uint8_t enc)
{
    g_encoding = enc;
    g_remap = (enc == ENC_CP1251) ? cp1251_to_866 :
              (enc == ENC_KOI8R)  ? koi8r_to_866  : (const uint8_t *)0;
}

/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты
 * различают 8-битные кодировки по частоте. */
// static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB};
// static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB};
// static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC};
// static uint8_t in_set10(const uint8_t *s, uint8_t b)
// {
//     for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1;
//     return 0;
// }

/* Проверяем только пять самых популярных символов (о е а и н) */
static const uint8_t common866 [5] = {0xAE,0xA5,0xA0,0xA8,0xAD};
static const uint8_t common1251[5] = {0xEE,0xE5,0xE0,0xE8,0xED};
static const uint8_t commonkoi [5] = {0xCF,0xC5,0xC1,0xC9,0xCE};
static uint8_t in_set10(const uint8_t *s, uint8_t b)
{
    for (uint8_t i = 0; i < 5; i++) if (s[i] == b) return 1;
    return 0;
}

/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса).
 * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по
 * частоте ходовых букв; фолбэк CP866. */
uint8_t detect_encoding(void)
{
    if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF)
        return ENC_UTF8;

    uint32_t n = file_size;
    if (n > 1024u) n = 1024u;                    /* сэмпл: первый 1 КБ — детекции хватает */
    uint8_t  utf_ok = 1, has_mb = 0, has_high = 0, cont = 0;
    uint16_t s866 = 0, s1251 = 0, skoi = 0;

    for (uint32_t p = 0; p < n; p++) {
        uint8_t b = (uint8_t)fb(p);
        if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; }
        has_high = 1;
        if (in_set10(common866,  b)) s866++;
        if (in_set10(common1251, b)) s1251++;
        if (in_set10(commonkoi,  b)) skoi++;
        if (cont) {
            if ((b & 0xC0) == 0x80) cont--;
            else { utf_ok = 0; cont = 0; }
        } 
        else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; }
        else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; }
        else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; }
        else utf_ok = 0;                       /* битый лид/одиночный континюэйшн */
    }
    /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только
     * если это настоящий EOF; на границе сэмпла (n<file_size) это просто
     * обрезка, не считаем за ошибку. */
    if (cont && n == file_size) utf_ok = 0;

    if (!has_high) return ENC_CP866;             /* чистый ASCII */
    if (utf_ok && has_mb) return ENC_UTF8;
    if (s1251 >= s866 && s1251 >= skoi) return ENC_CP1251;
    if (skoi  >= s866) return ENC_KOI8R;
    return ENC_CP866;
}

const char *enc_name(uint8_t enc)
{
    return (enc == ENC_CP1251) ? "CP1251" :
           (enc == ENC_KOI8R)  ? "KOI8-R" :
           (enc == ENC_UTF8)   ? "UTF-8 " : "CP866 ";
}

/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/
 * маркеры — уже CP866, не трогаем). Различаем по attr. */
static uint8_t attr_is_content(uint8_t a)
{
    return (uint8_t)(a != ATTR_BOX && a != ATTR_HR &&
                     a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER);
}

/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через
 * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */
void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off)
{
    if (!g_remap || w == 0) { 
        win_rest(row, 0, 1, w, page, off); 
        return; 
    }
    uint8_t buf[SCREEN_W * 2];
    bank_read(page, off, buf, (uint16_t)w * 2u);
    for (uint8_t i = 0; i < w; i++) {
        uint8_t ch = buf[(uint16_t)i * 2u];
        if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u]))
            buf[(uint16_t)i * 2u] = g_remap[ch - 0x80];   /* таблицы хранят только старшие 128 */
    }
    bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u);
    win_rest(row, 0, 1, w, g_scratch_phys, 0);
}

/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */

/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских
 * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */
static const uint8_t utf_cyr_to_866[96] = {
    /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F,
    /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
    /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
    /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
    /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
    /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F,
};

/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в
 * near-памяти, флаш через один bank_write (не побайтовый swap W3). */
static uint8_t  conv_buf[256];
static uint16_t conv_n;   /* байт в буфере, ещё не сброшено */
static uint8_t  conv_page;   /* индекс текущей страницы в utf_phys */
static uint16_t conv_off;   /* уже сброшенное смещение в этой странице */

static void conv_flush(void)
{
    if (conv_n) {
        bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n);
        conv_off = (uint16_t)(conv_off + conv_n);
        conv_n   = 0;
    }
}

/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает
 * границу страницы (256 делит 16384 нацело). */
static void conv_put(uint8_t b)
{
    if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) {  /* страница заполнена */
        conv_flush();
        conv_page++;
        conv_off = 0;
    }
    conv_buf[conv_n++] = b;
    if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush();
}

/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт.
 * Таблица вместо switch (экономия кода); линейный поиск дёшев — вызывается
 * лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый
 * символ — достаточно дописать одну строку {кодпойнт, байт-CP866}.
 * (Символы с НЕ-1:1 заменой, напр. … -> "...", обрабатываются отдельно ниже.) */
typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t;
static const utf_sym_t utf_sym[] = {
    {0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp «  »  → space < > */
    {0x00B0, 0xF8}, {0x00B7, 0xFA},                 /* °  ·                   */
    {0x2013, 0x2D}, {0x2014, 0x2D},                 /* –  —  → '-'            */
    {0x2018, 0x27}, {0x2019, 0x27},                 /* ‘  ’  → '\''           */
    {0x201C, 0x22}, {0x201D, 0x22},                 /* “  ”  → '"'            */
    {0x2022, 0xF9}, {0x2116, 0xFC},                 /* •  → ∙   №             */
    {0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */
    {0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */
    {0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */
    {0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5},                 /* ┬ ┴ ┼   */
    {0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB},                 /* ▀ ▄ █   */
    {0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */
    {0x2713, 0xFB}, {0x2714, 0xFB},                 /* ✓  ✔  → галка          */
    {0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕  ✗  ✘  → 'x'         */
};
#define UTF_SYM_N  (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0]))

/* Кодпойнт → ОДИН CP866-глиф: кириллица по таблице, ходовые символы по
 * utf_sym[], прочее → '?'. Общая для конвертера и HEX-printable (F4). */
uint8_t utf_cp_glyph(uint32_t cp)
{
    if (cp < 0x80) return (uint8_t)cp;
    if (cp >= 0x0400 && cp <= 0x045F) return utf_cyr_to_866[cp - 0x0400];
    for (uint8_t i = 0; i < UTF_SYM_N; i++)
        if (utf_sym[i].utf8 == (uint16_t)cp) return utf_sym[i].cp866;
    return '?';
}

/* Кодпойнт → CP866-глиф(ы) в поток конвертера. Спецслучаи с НЕ-1:1 заменой
 * (… → "...", BOM → ничего) здесь; остальное через utf_cp_glyph(). */
static void conv_emit_cp(uint32_t cp)
{
    if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; }  /* … → "..." */
    if (cp == 0xFEFF) return;                                /* BOM/ZWNBSP — выкинуть */
    conv_put(utf_cp_glyph(cp));
}

/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит
 * в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной
 * cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и
 * чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */
static uint8_t cv_read(uint32_t s)
{
    uint8_t pg = (uint8_t)(s >> PAGE_BITS);
    if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; }
    return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK)));
}

/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока
 * сконвертированных (flushed) байт меньше target и не достигнут конец
 * исходника. По выходу file_size = доступная (сконвертированная) часть; при
 * достижении конца фиксирует utf_size и снимает g_utf_building. */
void utf_convert_more(uint32_t target)
{
    cv_page = 0xFF;                 /* W3 был на utf (fb индексатора) — пере-смаппим orig */
    uint32_t n = orig_file_size;
    while (utf_src < n &&
           (uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) {
        uint8_t b = (uint8_t)cv_read(utf_src++);
        if (b < 0x80) { 
            conv_emit_cp(b); 
            continue; 
        }

        uint32_t cp;
        uint8_t  need;
        if      ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; }
        else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; }
        else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; }
        else { conv_put('?'); continue; }

        uint8_t ok = 1;
        for (uint8_t k = 0; k < need; k++) {
            if (utf_src >= n) { ok = 0; break; }
            uint8_t cb = (uint8_t)cv_read(utf_src);
            if ((cb & 0xC0) != 0x80) { ok = 0; break; }
            cp = (cp << 6) | (uint32_t)(cb & 0x3F);
            utf_src++;
        }
        if (!ok || need == 3) { conv_put('?'); continue; }
        conv_emit_cp(cp);
    }
    conv_flush();
    file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off;   /* доступно индексатору */
    if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; }
    cur_page = 0xFF;               /* W3 был на orig — fb индексатора пере-смаппит utf */
}

/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру)
 * и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src,
 * g_utf_building). Сама конвертация — инкрементальная, через utf_convert_more().
 * Возврат: 1 — успех, 0 — нет EMM. */
uint8_t utf_alloc(void)
{
    utf_pages = orig_file_pages;
    utf_blk   = mem_alloc_pages(utf_pages);
    if (utf_blk == 0) return 0;
    for (uint8_t i = 0; i < utf_pages; i++)
        utf_phys[i] = mem_get_page(utf_blk, i);
    conv_n = 0; conv_page = 0; conv_off = 0;   /* converter reset (вызывается один раз) */
    utf_src = 0;
    g_utf_building = 1;
    return 1;
}
