/*
 * sprite.h — managed-движок спрайтов (retained-модель) поверх ядра
 * gfx_blit/gfx_heal.  Дизайн: docs/sprite-api-design.md §9.1.
 *
 * Мотивация — profile examples/balls vs asm-референса docs/samples/balls
 * (2026-07-12): ~2× разрыв не в W3-скобке, а в per-call C-обвязке
 * (клип/парс заголовка/save-restore банка/SDCC-фрейм), повторяемой 2×N
 * раз за кадр.  Движок амортизирует её: sprite_update держит ОДНУ
 * W3-скобку и ОДИН банк на весь ПРОХОД, вызывая bracket-free ядра
 * (_gfx_heal_full/_gfx_blit_full) в цикле по спрайтам.  DI остаётся
 * гранулярным (по строке в leaf) — под CBL/IM2-аудио длинный DI
 * недопустим.
 *
 * Retained-модель: sprite_move/frame/show/hide/touch меняют ТОЛЬКО
 * состояние (O(1), сколько угодно раз за кадр); всё рисование —
 * один раз в sprite_update, двухпроходно «heal ВСЕ → блит ВСЕ» (слияние
 * per-sprite выкусывает соседей на перекрытии — проверено).  Учёт
 * «где спрайт реально нарисован на КАЖДОЙ из двух страниц» (drawn[2])
 * ведёт движок, не приложение.
 *
 * Формат картинки — getimage (uint16 w, uint16 h, пиксели построчно,
 * 1 байт/пиксель в 256-режиме); 0xFF = прозрачно (банк 0x5C).  Буфер
 * обязан лежать ВНЕ W3 (< 0xC000).  Размер КАДРА спрайта ≤ 64×64
 * (требование движка): кэш w,h — uint8_t, и блит идёт лин-ядром без
 * split'а на полосы >256 (спрайт заведомо ≤256 в строке).  Для больших
 * картинок — gfx_blit/putimage напрямую (там общий путь со split'ом).
 * ЛЕНТА-АТЛАС может быть шире 64 (img_w — шаг), но КАДР ≤ 64.
 *
 * z-order = индекс в массиве (последний рисуется сверху).  Массив
 * владеет приложением: без malloc, без лимитов; пересортировка z =
 * перестановка элементов (drawn-состояние едет со структурой).
 */
#ifndef SPRITE_H
#define SPRITE_H

#include <stdint.h>

/* Публичные флаги sprite_t.flags. */
#define SPR_VISIBLE  0x01u   /* спрайт рисуется в sprite_update            */
#define SPR_ALWAYS   0x02u   /* перерисовывать КАЖДЫЙ кадр (статик, над    */
                             /* которым постоянно ходят активные спрайты) */

/* Внутренние dirty-биты (владеет движок): «страница p требует обновления
 * этого спрайта».  Приложению не трогать — только через sprite_*. */
#define _SPR_DIRTY0  0x04u
#define _SPR_DIRTY1  0x08u
#define _SPR_DIRTY   (_SPR_DIRTY0 | _SPR_DIRTY1)

/* Статус-биты sprite_t.an_flags (чтение — sprite_anim_status()):
 * *_ON — анимация активна (тикается в sprite_update); *_DONE — дошла
 * до конца сама (one-shot до last / перемещение до цели) или была
 * добита sprite_move_stop(at_target=1).  DONE сбрасывается новым
 * запуском соответствующей анимации. */
#define SPR_ANIM_ON    0x01u  /* кадровая анимация идёт                 */
#define SPR_ANIM_DONE  0x02u  /* one-shot дошёл до last                 */
#define SPR_MOVE_ON    0x04u  /* tween-перемещение идёт                 */
#define SPR_MOVE_DONE  0x08u  /* перемещение достигло цели              */

/* Внутренние биты an_flags (владеет движок) — три РАЗНЫЕ категории:
 * режим анимации (зеркало параметра mode), свойство ЛЕНТЫ (ориентация
 * данных в атласе) и runtime-состояние (меняется само в процессе). */
#define _SPR_PP_BACK    0x10u  /* runtime: маятник сейчас идёт назад    */
#define _SPR_STRIP_HORZ 0x20u  /* лента: кадры вдоль X (двигается sx)   */
#define _SPR_ANIM_PP    0x40u  /* режим: маятник                        */
#define _SPR_ANIM_ONCE  0x80u  /* режим: one-shot                       */

/* Режимы sprite_anim(). */
#define ANIM_LOOP      0u     /* 0,1,..,last,first,..  (цикл)           */
#define ANIM_PINGPONG  1u     /* 0,1,..,last,last-1,..,first,1,..       */
#define ANIM_ONCE      2u     /* first..last, замереть на last + DONE   */
#define ANIM_HORIZ     0x80u  /* ИЛИ-флаг: кадры вдоль X (сетка nx>1)   */

/* ВНИМАНИЕ: порядок полей ЗАШИТ в asm-ядро тикера (офсеты полей в
 * common/_sprite_tick.c, tick_move) — при изменении порядка/типов
 * пересчитать офсеты там (offsetof-пробником, рецепт в его шапке). */
typedef struct {
    const void *img;     /* getimage-формат / атлас-лента (вне W3)         */
    /* --- кэш адреса кадра (владеет движок; менять ТОЛЬКО через
     *     sprite_frame) ---
     * src — ГОТОВЫЙ адрес первого пикселя ТЕКУЩЕГО кадра (img + 4 +
     * sx + sy*stride).  Блит берёт его как есть: ни чтения заголовка,
     * ни умножения в кадровом цикле.  Тикер анимации двигает src
     * инкрементально (±an_step байт); sx/sy при этом НЕ обновляются —
     * они «последняя позиция, установленная sprite_frame», и снова
     * становятся истиной при следующем sprite_frame/sprite_anim
     * (те ставят ось абсолютно, а не относительно). */
    const uint8_t *src;  /* адрес пикселей текущего кадра                  */
    uint16_t stride;     /* полная ширина img в байтах (шаг строки)        */
    int   x, y;          /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра  */
    int   sx, sy;        /* смещение кадра, заданное sprite_frame (см. src)*/
    uint8_t w, h;        /* размер кадра (кэш заголовка), 1..255           */
    uint8_t flags;       /* SPR_* | внутренние dirty                       */
    uint8_t page;        /* 0 = img в обычной памяти; иначе физ. страница  *
                          * W0-атласа (img — адрес 0x0100-0x3FFF, движок   *
                          * сам мапит страницу в W0 на время блита)        */
    /* --- авто-анимация (владеет тикер; настраивать ТОЛЬКО через
     *     sprite_anim../sprite_moveto.., читать — inline-аксессорами) --- */
    uint8_t an_flags;    /* статус: SPR_ANIM_.. | SPR_MOVE_..              */
    uint8_t an_frame;    /* ТЕКУЩИЙ индекс кадра (0-based по ленте)        */
    uint8_t an_first, an_last;   /* границы анимации (индексы)             */
    uint8_t an_speed, an_timer;  /* кадров между сменами / счётчик         */
    uint16_t an_step;    /* шаг кадра в БАЙТАХ src: fw (гориз. лента)      *
                          * или fh*stride (верт.) — считает sprite_anim    */
    const uint8_t *an_src0;      /* src кадра first (для wrap цикла)       */
    uint8_t mv_speed, mv_timer;  /* кадров между шагами / счётчик          */
    uint8_t mv_step;     /* макс. пикселей вдоль большей оси за шаг        */
    int     mv_tx, mv_ty;        /* цель (для sprite_move_stop)            */
    /* DDA по главной оси (беззнаковый — на Z80 16-бит беззнаковые
     * сравнения в разы дешевле знаковых Брезенхэма, 2026-07-14):
     * главная ось шагает каждую итерацию, минорная подтягивается
     * аккумулятором; «приехали» = mv_rem == 0 (сравнений координат нет). */
    uint16_t mv_rem;     /* осталось шагов главной оси до цели             */
    uint16_t mv_acc;     /* аккумулятор минорной оси (старт dmaj/2)        */
    uint16_t mv_dmaj, mv_dmin;   /* |дельты| главной / минорной осей       */
    uint8_t  mv_ymaj;    /* 1 = главная ось Y (иначе X)                    */
    int8_t  mv_sgnmaj, mv_sgnmin;/* знаки шага главной/минорной осей       */
    /* --- внутреннее (владеет движок) --- */
    struct {
        int x, y;        /* где нарисован на странице p                    */
        uint8_t on;      /* нарисован ли на странице p                     */
    } drawn[2];
    /* Слой для Y-сортировки (см. gfx_sprite_ysort): при включённой
     * сортировке спрайты упорядочиваются по {layer, y} — слой старше:
     * слой 1 ВСЕГДА поверх слоя 0 (ходячие/летающие/курсор...), внутри
     * слоя — painter по y.  0 после sprite_init; без сортировки не
     * используется (порядок = индекс массива).  Поле В КОНЦЕ структуры:
     * офсеты полей выше зашиты в asm тикера/сортировщика. */
    uint8_t layer;
} sprite_t;

/* Инициализация: w,h из заголовка img, кадр (0,0), невидим, ничего не
 * нарисовано.  Для АТЛАСА (кадр — под-прямоугольник) после init
 * выставить s->w/s->h в размер кадра и перейти на нужный кадр ТОЛЬКО
 * через sprite_frame (он ведёт кэш src; голые s->sx/s->sy кэш не
 * обновят). */
void sprite_init(sprite_t *s, const void *img);

/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
 * heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
 * скобка/банк на проход).  Спрайты без изменений с прошлого визита этой
 * страницы (и без SPR_ALWAYS) не трогаются.  Для дабл-буфера звать после
 * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
 * видимой.
 *
 * ВСЯ СЦЕНА СТРАНИЦЫ — ОДИН ВЫЗОВ!  Два sprite_update на одну страницу
 * (например «сначала ходячие, потом летающие») ЛОМАЮТ перекрытия МЕЖДУ
 * группами: heal второй группы берёт из ОЗУ-копии чистый ФОН (спрайты
 * в копию не пишутся — банк 0x5C) и стирает нарисованных первой
 * группой; двухпроходная дисциплина «heal все → блит все» покрывает
 * перекрытия только ВНУТРИ одного вызова.  Логические группы держать
 * диапазонами индексов одного массива (z-порядок групп = порядок
 * диапазонов; слоение при Y-сортировке — полем layer). */
void sprite_update(sprite_t *arr, uint8_t count);

/* Сахар канонического дабл-буфер-кадра: draw-страница = скрытая →
 * sprite_update → gfx_wait_vsync → сделать скрытую видимой (tear-free).
 * Динамический фон/HUD рисовать банком 0x50 на скрытой странице ДО
 * sprite_flip — тогда heal учтёт его через ОЗУ-копию. */
void sprite_flip(sprite_t *arr, uint8_t count);

/* Режимы gfx_sprite_ysort (битовая маска компонентов ключа). */
#define YSORT_OFF    0u      /* сортировки нет: z-order = индекс массива */
#define YSORT_Y      0x01u   /* painter по y (нижние поверх верхних)     */
#define YSORT_LAYER  0x02u   /* слои: layer 1 поверх layer 0             */

/* Y-сортировка блит-прохода (painter's algorithm): mode — какие
 * компоненты ключа участвуют в порядке отрисовки:
 *   YSORT_Y | YSORT_LAYER — полный порядок {layer, y}: сначала слои
 *       (спрайт слоя 1 ВСЕГДА поверх слоя 0 — ходячие/летающие/
 *       курсор...), внутри слоя — по y (нижние по экрану поверх
 *       верхних, «ближе к зрителю»);
 *   YSORT_Y      — только painter по y, слои игнорируются;
 *   YSORT_LAYER  — только слои; внутри слоя порядок стабилен
 *       (наследуется от прошлого кадра / порядка массива);
 *   YSORT_OFF (0, дефолт) — z-order = индекс массива.
 * Выключенный компонент маскируется в ключе (2 AND на спрайт, ~1К на
 * 15 спрайтов) — сортировщик не замедляется ветвлениями.
 *
 * ПЕРСИСТЕНТНОСТЬ: таблица порядка живёт МЕЖДУ кадрами — каждый кадр
 * ключи обновляются и порядок пересортировывается вставками; спрайты
 * двигаются на пиксели за кадр, порядок почти актуален → почти
 * линейно.  Цена (замер rpgwalk-15, 2026-07-14): ~28К тактов на 15
 * спрайтов (~2К/спрайт, ~6% бюджета кадра; построение с нуля каждый
 * кадр стоило 44К).  Массив приложения при этом НЕ трогается (sp[2]
 * всегда персонаж 2) — сортируются внутренние записи-указатели.  При
 * смене arr/count (другая сцена) таблица перестраивается сама; первый
 * кадр после включения/смены — полная сортировка (разово дороже).
 *
 * Сортировка СТАБИЛЬНА относительно ПРОШЛОГО КАДРА: спрайты с равным
 * ключом сохраняют взаимный порядок прошлого кадра (не мерцают), а не
 * порядок массива.  Лимит: до 32 спрайтов на вызов sprite_update;
 * больше — кадр рисуется без сортировки (безопасный фолбэк).
 * Выключено = 0 тактов И 0 байт: код сортировщика и таблица не
 * линкуются (funcptr-DCE).  heal-проход порядка не требует и остаётся
 * линейным.
 *
 * Замечания: (1) перекрывающиеся спрайты обязаны быть dirty ОБА, чтобы
 * смена их взаимного порядка отрисовалась — движущиеся грязнятся сами,
 * статики помечать sprite_touch (правило то же, что и без сортировки);
 * (2) сортировка перемешивает порядок W0-атласов — смен страницы W0
 * может стать больше (один дешёвый OUT на смену). */
void gfx_sprite_ysort(uint8_t on);

/* --- O(1) изменения состояния (inline: без call-оверхеда в физике) --- */

/* Переместить: меняет только логическую позицию + грязнит обе страницы. */
inline void sprite_move(sprite_t *s, int x, int y) {
    if (s->x != x || s->y != y) { s->x = x; s->y = y; s->flags |= _SPR_DIRTY; }
}

/* Сменить кадр атласа (под-прямоугольник img).  Не inline: пересчитывает
 * кэш src (одно 16-бит умножение sy*stride) — при АКТИВНОЙ кадровой
 * анимации не звать (тикер ведёт src сам). */
void sprite_frame(sprite_t *s, int sx, int sy);

/* Показать (если был скрыт — грязнит обе страницы). */
inline void sprite_show(sprite_t *s) {
    if (!(s->flags & SPR_VISIBLE)) s->flags |= (SPR_VISIBLE | _SPR_DIRTY);
}

/* Скрыть (если был виден — грязнит: sprite_update сотрёт heal'ом). */
inline void sprite_hide(sprite_t *s) {
    if (s->flags & SPR_VISIBLE) { s->flags &= ~SPR_VISIBLE; s->flags |= _SPR_DIRTY; }
}

/* Принудительная перерисовка на ОБЕИХ страницах (напр. после того как
 * активный спрайт прошёл над статиком — приложение помечает статик). */
inline void sprite_touch(sprite_t *s) { s->flags |= _SPR_DIRTY; }

/* ==== Авто-анимация (§9г дизайна) ====================================
 * Тикается САМА в начале каждого sprite_update (раз на кадр), код
 * тикера подключается через funcptr — программа без анимаций его не
 * линкует.  Кадровая анимация и tween-перемещение НЕЗАВИСИМЫ и могут
 * идти одновременно.  Пока кадровая активна — не трогать sprite_frame
 * вручную (тикер ведёт ось сам); границы кадров должны лежать вдоль
 * ОДНОЙ оси ленты (вертикальная по умолчанию, ANIM_HORIZ — вдоль X). */

/* Запустить кадровую анимацию: кадры first..last (индексы вдоль оси
 * ленты), смена раз в speed кадров (1 = каждый кадр), mode = ANIM_LOOP/
 * ANIM_PINGPONG/ANIM_ONCE (| ANIM_HORIZ для горизонтальной ленты).
 * Старт с first; сбрасывает SPR_ANIM_DONE. */
void sprite_anim(sprite_t *s, uint8_t first, uint8_t last,
                 uint8_t speed, uint8_t mode);

/* Остановить кадровую анимацию: frame = индекс кадра, который останется
 * на экране, или -1 — замереть на текущем. */
void sprite_anim_stop(sprite_t *s, int8_t frame);

/* Запустить перемещение к (tx,ty): раз в interval кадров — шаг до
 * max_step пикселей вдоль БОЛЬШЕЙ оси (меньшая тянется Брезенхэмом,
 * без деления).  Сбрасывает SPR_MOVE_DONE.  Пример: (0,0)→(100,50),
 * max_step 5, interval 4 → 20 шагов × 4 кадра = 80 кадров (1.6 с). */
void sprite_moveto(sprite_t *s, int tx, int ty,
                   uint8_t max_step, uint8_t interval);

/* Остановить перемещение: at_target = 0 — замереть где есть;
 * 1 — прыгнуть в цель (и взвести SPR_MOVE_DONE). */
void sprite_move_stop(sprite_t *s, uint8_t at_target);

/* Статус анимаций: битовое поле SPR_ANIM_ON/DONE | SPR_MOVE_ON/DONE. */
inline uint8_t sprite_anim_status(const sprite_t *s) { return s->an_flags; }

/* Текущий индекс кадра ленты (ведёт тикер; валиден и после stop). */
inline uint8_t sprite_anim_frame(const sprite_t *s) { return s->an_frame; }

/* Перемещение ещё идёт? */
inline uint8_t sprite_moving(const sprite_t *s) {
    return s->an_flags & SPR_MOVE_ON;
}

/* ==== Атласы в EMM-страницах (W0) ====================================
 * Файл .atl (docs/sprite-api-design.md §3.1, вариант II): 0x100-байтовый
 * заголовок (магия 'SPA1', count; каталог с 0x68) + getimage-ленты;
 * файл-офсет == офсет в странице == адрес в W0.  Загрузчик читает файл
 * ЦЕЛИКОМ в EMM-страницу через W3 и патчит ISR-стаб (0x38/0x66) —
 * страница безопасна в W0 при включённых прерываниях (§9в).
 *
 * ВАЖНО: указатели лент — адреса 0x0100-0x3FFF, разыменовывать их можно
 * ТОЛЬКО при подключенной странице (sprite_update мапит сам; для ручного
 * putsprite/gfx_blit_part — обернуть в gfx_w0_map/gfx_w0_unmap).
 * Пока страница в W0 — НИКАКИХ ESTEX/BIOS-вызовов. */

typedef struct {
    uint8_t page;        /* физ. страница (маппить в W0/W3)     */
    uint8_t blk;         /* EMM-блок — для atlas_free           */
    uint8_t count;       /* число лент в атласе                 */
} atlas_t;

/* Загрузить .atl в свежую EMM-страницу (read в W3 + патч стаба).
 * 0 — OK; -1 + errno (файл/память/формат). */
int  atlas_load(atlas_t *a, const char *path);

/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
 * быть скрыты/переинициализированы ДО вызова). */
void atlas_free(atlas_t *a);

/* Указатель-лента idx (W0-адрес, см. ВАЖНО выше) — для gfx_blit_part
 * и ручных вызовов; кадр (i,j) ленты: sx = i*fw, sy = j*fh. */
const void *atlas_image(const atlas_t *a, uint8_t idx);

/* Инициализировать спрайт лентой idx атласа: img/w/h (размер КАДРА из
 * каталога) /page; невидим, кадр (0,0).  Аналог sprite_init для W0. */
void atlas_sprite_init(sprite_t *s, const atlas_t *a, uint8_t idx);

/* Ручное подключение страницы атласа в W0 (для putsprite/gfx_blit_part
 * вне движка).  map ставит и _gfx_w0_cur (ISR-стаб восстановит страницу
 * после прерывания), unmap возвращает страницу ядра DSS. */
void gfx_w0_map(uint8_t page);
void gfx_w0_unmap(void);

typedef struct {
    int16_t x, y;
    uint16_t w, h;
} gfx_rect_t;

/* Скролл региона area из НЕактивной страницы в активную со сдвигом на
 * dx/dy (0 = чистая копия/heal, >0 = в сторону увеличения координат,
 * <0 = уменьшения).  Открывшуюся полосу |d| НЕ заполняют — возвращают
 * в *dirty (может быть NULL).  Банк 0x50: копия = скролл + heal цели. */
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty);
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty);

#endif
