/*
 * pop_level.c — загрузка уровня PoP из сырого res200N.bin (Фаза L1).
 * См. pop_level.h.  Порт формата blueprnt DAT 1.0 (Table 6 POP-DAT).
 */
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#include <sprite.h>          /* gfx_w0_map / gfx_w0_unmap */
#include "pop_level.h"

/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
 * замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
 * _gfx_w0_isr (восстановит окно).  Символ внутренний для libbgi, но
 * стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
extern void _gfx_w0_isr(void);

#define LVL_DATA_OFF 0x100u  /* данные с 0x100; 0x00..0xFF — ISR-стаб */
/* Эталонная копия foretable В ТОЙ ЖЕ странице: страница 16 КБ, данных
 * уровня 2.3 КБ — место даром.  Нужна, потому что foretable теперь
 * МУТАБЕЛЬНАЯ (pop_level_set_tile), а рестарт уровня в оригинале —
 * это load_level() заново (play_level, seg003:57), то есть все плиты целы
 * и зелья на месте.  pop_level_reset_tiles восстанавливает из неё. */
#define LVL_PRISTINE_OFF 0x1000u

/* Смещения блоков blueprnt относительно начала данных (Table 6). */
#define BP_FG      0u
#define BP_BG      720u
#define BP_LINKLOC 1440u   /* doorlink1 (LINKLOC), 256 Б */
#define BP_LINKMAP 1696u   /* doorlink2 (LINKMAP), 256 Б */
#define BP_LINKS   1952u
/* Стражи (level_type, types.h:228): по байту на комнату в каждом массиве. */
#define BP_GTILE   2119u
#define BP_GDIR    2143u
#define BP_GX      2167u
#define BP_GSEQLO  2191u
#define BP_GSKILL  2215u
#define BP_GSEQHI  2239u
#define BP_GCOLOR  2263u
#define BP_START   2112u
#define ROOM_TILES 30u
#define MIN_SIZE   (BP_START + 3u)   /* минимум, чтобы прочитать старт */

/* fwd: инициализация живой копии стражей (тело ниже). */
static void gstate_init(void);


static uint8_t lvl_blk, lvl_page, lvl_ok;
/* Копии таблиц дверных связей в W2 (LINKMAP мутабельна — таймеры кнопок). */
static uint8_t dl1[256], dl2[256];

int pop_level_load(const char *path)
{
    uint8_t *pg = (uint8_t *)0xC000;   /* страница мапится в W3 на время чтения */
    uint8_t saved_w3, blk;
    uint16_t stub;
    int fd, n;

    fd = open(path, O_RDONLY);
    if (fd < 0) return -1;

    blk = mem_alloc_pages(1);
    if (!blk) { close(fd); return -1; }
    lvl_blk  = blk;
    lvl_page = mem_get_page(blk, 0);

    saved_w3 = _io_page_w3;
    sprinter_page_w3(lvl_page);
    /* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
    n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
    if (n >= (int)MIN_SIZE) {                 /* пропатчить ISR-стаб */
        uint16_t i;
        stub = (uint16_t)&_gfx_w0_isr;
        pg[0x38] = 0xC3;                      /* JP _gfx_w0_isr */
        pg[0x39] = (uint8_t)(stub & 0xFF);
        pg[0x3A] = (uint8_t)(stub >> 8);
        pg[0x66] = 0xED; pg[0x67] = 0x45;     /* RETN (NMI-хвост) */
        /* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
        for (i = 0; i < 256; i++) {
            dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
            dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
        }
        /* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
        for (i = 0; i < 24u * ROOM_TILES; i++)
            pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
    }
    sprinter_page_w3(saved_w3);
    close(fd);

    if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
    lvl_ok = 1;
    gstate_init();          /* живая копия состояния стражей — из уровня */
    return 0;
}

void pop_level_free(void)
{
    if (lvl_ok) { mem_free_block(lvl_blk); lvl_ok = 0; }
}

/* ---- Номер уровня и потабличные различия ---------------------------- */

uint8_t pop_current_level;

/* Таблицы из SDLPoP data.h:840..848, индекс = номер уровня (0 = демо).
 * Держим целиком по 16 байт: короче и дешевле любых «оптимизаций», а
 * расхождение с оригиналом сразу видно глазами. */
static const uint8_t tbl_entry_pose[16] =
    { 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0 };
static const uint8_t tbl_guard_hp[16] =
    { 4, 3, 3, 3, 3, 4, 5, 4, 4, 5, 5, 5, 4, 6, 0, 0 };
static const int8_t  tbl_guard_type[16] =
    { 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 4, 3, -1, -1 };
static const uint8_t tbl_level_type[16] =
    { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0 };

uint8_t pop_level_entry_pose(uint8_t n) { return n < 16 ? tbl_entry_pose[n] : 0; }
uint8_t pop_level_guard_hp(uint8_t n)   { return n < 16 ? tbl_guard_hp[n] : 3; }
int8_t  pop_level_guard_type(uint8_t n) { return n < 16 ? tbl_guard_type[n] : -1; }
uint8_t pop_level_tileset(uint8_t n)    { return n < 16 ? tbl_level_type[n] : 0; }

int pop_level_load_num(uint8_t n)
{
    /* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
     * резидент W1 форматтер, которого здесь больше негде применить. */
    static char pri[] = "LEVELS\\res2000.bin";   /* цифры — [12],[13] */
    static char alt[] = "a:\\res2000.bin";       /* цифры — [8],[9]   */
    uint8_t saved_blk = lvl_blk, saved_page = lvl_page, saved_ok = lvl_ok;

    if (n > 15) return -1;
    pri[12] = alt[8] = (char)('0' + n / 10);
    pri[13] = alt[9] = (char)('0' + n % 10);

    /* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
     * pop_level_load сам аллоцирует страницу, и если файла нет — остаёмся
     * на текущем уровне вместо падения в пустой уровень. */
    lvl_ok = 0;                          /* чтобы pop_level_load не мешался */
    if (pop_level_load(pri) != 0 && pop_level_load(alt) != 0) {
        lvl_blk = saved_blk; lvl_page = saved_page; lvl_ok = saved_ok;
        return -1;
    }
    if (saved_ok) mem_free_block(saved_blk);
    pop_current_level = n;
    return 0;
}

/* Указатель на fg-тайлы комнаты r (1..24) в W0-пространстве (страница уже
 * замаплена вызывающим). */
static const uint8_t *room_fg_ptr(uint8_t r)
{
    return (const uint8_t *)(LVL_DATA_OFF + BP_FG) + (uint16_t)(r - 1) * ROOM_TILES;
}
static const uint8_t *room_bg_ptr(uint8_t r)
{
    return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
}

int pop_room_load(uint8_t room, uint8_t *fg, uint8_t *bg,
                  uint8_t *lcol_fg, uint8_t *lcol_bg,
                  uint8_t *rcol_fg, uint8_t *rcol_bg, uint8_t *below_fg)
{
    const uint8_t *rfg, *rbg, *lnk;
    uint8_t left, right, down, i;

    if (!lvl_ok || room < 1 || room > 24) return -1;

    gfx_w0_map(lvl_page);
    rfg = room_fg_ptr(room);
    rbg = room_bg_ptr(room);
    lnk = (const uint8_t *)(LVL_DATA_OFF + BP_LINKS) + (uint16_t)(room - 1) * 4;

    for (i = 0; i < ROOM_TILES; i++) {
        fg[i] = (uint8_t)(rfg[i] & 0x1F);   /* тип тайла (верх. биты — модиф., пока не нужны) */
        bg[i] = rbg[i];                     /* backtable как есть */
    }

    left  = lnk[0];  /* L */
    right = lnk[1];  /* R */
    down  = lnk[3];  /* D */

    /* leftcol: правый столбец (col9) комнаты СЛЕВА, строки 0..2 (кромка col0),
     * а в [3..5] — ПРЕДпоследний (col8).  Вторая колонка нужна коллизии: Kid,
     * стоящий В шве (curr_col = −1), смотрит вперёд на колонку −2, и без неё
     * там оказывалась мнимая стена (get_tile → TILE_WALL), которая выпихивала
     * его сквозь решётку.  Оригинал резолвит любую колонку через
     * find_room_of_tile (seg006:005D). */
    if (left >= 1 && left <= 24) {
        const uint8_t *lf = room_fg_ptr(left);
        const uint8_t *lb = room_bg_ptr(left);
        for (i = 0; i < 3; i++) {
            lcol_fg[i]     = (uint8_t)(lf[i * 10 + 9] & 0x1F);
            lcol_fg[3 + i] = (uint8_t)(lf[i * 10 + 8] & 0x1F);
            lcol_bg[i] = lb[i * 10 + 9];
        }
    } else {
        for (i = 0; i < 3; i++) { lcol_fg[i] = 20; lcol_fg[3 + i] = 20; lcol_bg[i] = 0; }
    }

    /* rightcol: левый столбец (col0) комнаты СПРАВА, строки 0..2 (кромка col9:
     * коллизия на правом шве + переход вправо) */
    if (right >= 1 && right <= 24) {
        const uint8_t *rf = room_fg_ptr(right);
        const uint8_t *rb = room_bg_ptr(right);
        for (i = 0; i < 3; i++) {
            rcol_fg[i]     = (uint8_t)(rf[i * 10 + 0] & 0x1F);
            rcol_fg[3 + i] = (uint8_t)(rf[i * 10 + 1] & 0x1F);   /* колонка 11 (см. выше) */
            rcol_bg[i] = rb[i * 10 + 0];
        }
    } else {
        for (i = 0; i < 3; i++) { rcol_fg[i] = 20; rcol_fg[3 + i] = 20; rcol_bg[i] = 0; }
    }

    /* belowrow: верхний ряд (row0) комнаты снизу */
    if (down >= 1 && down <= 24) {
        const uint8_t *df = room_fg_ptr(down);
        for (i = 0; i < 10; i++) below_fg[i] = (uint8_t)(df[i] & 0x1F);
    } else {
        for (i = 0; i < 10; i++) below_fg[i] = 20;                    /* нет комнаты снизу = стена */
    }

    gfx_w0_unmap();
    return 0;
}

int8_t pop_room_col_landing(uint8_t room, uint8_t col)
{
    const uint8_t *rf;
    int8_t row, res = -1;
    if (!lvl_ok || room < 1 || room > 24 || col > 9) return -1;
    gfx_w0_map(lvl_page);
    rf = room_fg_ptr(room);
    for (row = 0; row < 3; row++) {           /* кусок пролетает пустые ряды */
        uint8_t t = (uint8_t)(rf[row * 10 + col] & 0x1F);
        if (t == 0) continue;                 /* empty — дальше вниз */
        if (t == 1) res = row;                /* floor — приземление → debris */
        break;                                /* первый не-пустой тайл — стоп */
    }
    gfx_w0_unmap();
    return res;
}

/* set_tile-запись в ЖИВУЮ foretable уровня (порт curr_room_tiles[…] = …).
 * В оригинале уровень — обычная структура в ОЗУ, и do_pickup (seg006:1671),
 * remove_loose (seg007:0EB8), loose_land (seg007:11E8) правят её насовсем.
 * У нас уровень в EMM-странице — пишем прямо туда, поэтому изменение
 * переживает выход из комнаты (BUG-LVLSTATE-1).  Пишем БАЙТ ЦЕЛИКОМ:
 * старшие биты старого тайла (напр. 0x20 «solid loose») к новому типу
 * отношения не имеют. */
void pop_level_set_tile(uint8_t room, uint8_t tilepos, uint8_t tile)
{
    if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return;
    gfx_w0_map(lvl_page);
    ((uint8_t *)(LVL_DATA_OFF + BP_FG))[(uint16_t)(room - 1) * ROOM_TILES + tilepos]
        = tile;
    gfx_w0_unmap();
}

/* Рестарт уровня (порт load_level из цикла play_level, seg003:57): вернуть
 * foretable в исходное состояние из эталонной копии. */
void pop_level_reset_tiles(void)
{
    uint16_t i;
    uint8_t *dst;
    const uint8_t *src;
    if (!lvl_ok) return;
    gfx_w0_map(lvl_page);
    dst = (uint8_t *)(LVL_DATA_OFF + BP_FG);
    src = (const uint8_t *)LVL_PRISTINE_OFF;
    for (i = 0; i < 24u * ROOM_TILES; i++) dst[i] = src[i];
    gfx_w0_unmap();
}

/* Рестарт уровня, часть вторая (тот же load_level + pos_guards, seg003:83):
 * стражи — такая же часть данных уровня, как тайлы, и перезагрузка файла
 * возвращает их всех.  Убитый до смерти Кида страж после respawn снова жив
 * и стоит на исходном тайле; забывается и запомненная поза трупа
 * (guards_seq_hi = 0 -> «поднять стандартной стойкой»). */
void pop_level_reset_guards(void)
{
    if (!lvl_ok) return;
    gstate_init();
}

uint8_t pop_doorlink1(uint8_t i)          { return dl1[i]; }
uint8_t pop_doorlink2(uint8_t i)          { return dl2[i]; }
void    pop_doorlink2_set(uint8_t i, uint8_t v) { dl2[i] = v; }

uint8_t pop_level_tile(uint8_t room, uint8_t tilepos)
{
    uint8_t v;
    if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
    gfx_w0_map(lvl_page);
    v = (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
    gfx_w0_unmap();
    return v;
}

/* Батч-доступ: смаппить страницу уровня в W0 ОДИН раз, прочитать много
 * тайлов через pop_level_tile_raw, размаппить один раз.  Экономит N свапов
 * окна 0 в горячих циклах (pop_process_trobs звал pop_level_tile на каждый
 * trob каждый кадр).  Между begin/end НЕЛЬЗЯ рисовать/звать код, которому
 * нужно окно 0 в исходном состоянии — только сырые чтения уровня. */
void pop_level_access_begin(void) { if (lvl_ok) gfx_w0_map(lvl_page); }
void pop_level_access_end(void)   { if (lvl_ok) gfx_w0_unmap(); }

uint8_t pop_level_tile_raw(uint8_t room, uint8_t tilepos)
{
    if (!lvl_ok || room < 1 || room > 24 || tilepos >= ROOM_TILES) return 0;
    return (uint8_t)(room_fg_ptr(room)[tilepos] & 0x1F);
}

void pop_level_room_bg(uint8_t room, uint8_t *out30)
{
    const uint8_t *rbg;
    uint8_t i;
    if (!out30) return;
    if (!lvl_ok || room < 1 || room > 24) {
        for (i = 0; i < ROOM_TILES; i++) out30[i] = 0;
        return;
    }
    gfx_w0_map(lvl_page);
    rbg = room_bg_ptr(room);
    for (i = 0; i < ROOM_TILES; i++) out30[i] = rbg[i];
    gfx_w0_unmap();
}

uint8_t pop_room_link(uint8_t room, uint8_t side)
{
    uint8_t v;
    if (!lvl_ok || room < 1 || room > 24 || side > 3) return 0;
    gfx_w0_map(lvl_page);
    v = ((const uint8_t *)(LVL_DATA_OFF + BP_LINKS))[(uint16_t)(room - 1) * 4 + side];
    gfx_w0_unmap();
    return v;
}

static uint8_t start_byte(uint16_t off)
{
    uint8_t v;
    if (!lvl_ok) return 0;
    gfx_w0_map(lvl_page);
    v = ((const uint8_t *)(LVL_DATA_OFF))[off];
    gfx_w0_unmap();
    return v;
}

/* Страж комнаты (enter_guard, seg002:0112 + pos_guards, seg003).  Возврат
 * 0 = стража в комнате нет (tile >= 30).  X оригинал пересчитывает из
 * колонки (pos_guards), поэтому guards_x из файла не читаем. */
/* ---- ЖИВОЕ состояние стражей (порт level.guards_*) ------------------ *
 * В оригинале массивы стражей — часть структуры уровня в ОЗУ, и движок их
 * ПЕРЕПИСЫВАЕТ: leave_guard при выходе из комнаты кладёт туда текущие
 * позицию/направление/мастерство, а у МЁРТВОГО ещё и curr_seq.  Благодаря
 * этому, вернувшись, находишь труп там же, а не воскресшего стража.
 * У нас уровень лежит в EMM-странице только на чтение, поэтому держим
 * мутабельную копию в W2: 6 байт на комнату. */
#define GS_TILE 0
#define GS_DIR  1
#define GS_X    2
#define GS_SKILL 3
#define GS_SEQL 4
#define GS_SEQH 5
static uint8_t gstate[24 * 6];

static void gstate_init(void)
{
    const uint8_t *p = (const uint8_t *)LVL_DATA_OFF;
    uint8_t r;
    gfx_w0_map(lvl_page);
    for (r = 0; r < 24; r++) {
        uint8_t *g = gstate + (uint16_t)r * 6;
        g[GS_TILE]  = p[BP_GTILE + r];
        g[GS_DIR]   = p[BP_GDIR + r];
        g[GS_X]     = p[BP_GX + r];
        g[GS_SKILL] = p[BP_GSKILL + r];
        /* guards_seq_lo/hi в ФАЙЛЕ не используются — там 0xFF во всех
         * комнатах (оригинал чистит их в reset_level_unused_fields).
         * Осмысленное значение туда кладёт только leave_guard, поэтому
         * стартуем нулями: 0 = «поднимать стандартной стойкой».
         * Без этого в интерпретатор уходил curr_seq = 0xFFFF и он вис. */
        g[GS_SEQL]  = 0;
        g[GS_SEQH]  = 0;
    }
    gfx_w0_unmap();
}

/* leave_guard (seg002:02F5): запомнить состояние стража комнаты.  seq
 * сохраняем ТОЛЬКО у мёртвого — у живого seq_hi = 0 означает «поднять
 * стандартной стойкой», как в оригинале. */
void pop_guard_state_save(uint8_t room, uint8_t tile, int8_t dir, uint8_t x,
                          uint8_t skill, uint16_t seq, uint8_t dead)
{
    uint8_t *g;
    if (room < 1 || room > 24) return;
    g = gstate + (uint16_t)(room - 1) * 6;
    g[GS_TILE]  = tile;
    g[GS_DIR]   = (uint8_t)dir;
    g[GS_X]     = x;
    g[GS_SKILL] = skill;
    if (dead) { g[GS_SEQL] = (uint8_t)seq; g[GS_SEQH] = (uint8_t)(seq >> 8); }
    else      { g[GS_SEQH] = 0; }
}

uint16_t pop_guard_state_seq(uint8_t room)
{
    const uint8_t *g;
    if (room < 1 || room > 24) return 0;
    g = gstate + (uint16_t)(room - 1) * 6;
    if (g[GS_SEQH] == 0) return 0;          /* живой — стандартная стойка */
    return (uint16_t)(g[GS_SEQL] | ((uint16_t)g[GS_SEQH] << 8));
}

/* Запомненная X стража комнаты (для трупа: он лежит там, где лёг, а не в
 * центре тайла). */
uint8_t pop_guard_state_x(uint8_t room)
{
    if (room < 1 || room > 24) return 0;
    return gstate[(uint16_t)(room - 1) * 6 + GS_X];
}

uint8_t pop_level_guard(uint8_t room, uint8_t *tile, int8_t *dir,
                        uint8_t *color, uint8_t *skill)
{
    const uint8_t *g;
    uint8_t t;
    if (!lvl_ok || room < 1 || room > 24) return 0;
    /* Тайл/направление/мастерство — из ЖИВОЙ копии (её правит
     * pop_guard_state_save); цвет статичен, его читаем со страницы. */
    g = gstate + (uint16_t)(room - 1) * 6;
    t = g[GS_TILE];
    if (t < 30) {
        *tile  = t;
        *dir   = (int8_t)g[GS_DIR];
        *skill = g[GS_SKILL];
        gfx_w0_map(lvl_page);
        *color = (uint8_t)(((const uint8_t *)LVL_DATA_OFF)[BP_GCOLOR + room - 1] & 0x0F);
        gfx_w0_unmap();
    }
    return (uint8_t)(t < 30);
}

uint8_t pop_level_start_room(void) { return start_byte(BP_START + 0); }
uint8_t pop_level_start_pos(void)  { return start_byte(BP_START + 1); }
int8_t  pop_level_start_dir(void)  { return (int8_t)start_byte(BP_START + 2); }
