/*
 * 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)

typedef struct {
    const void *img;     /* getimage-формат / атлас-лента (вне W3)         */
    int   x, y;          /* ЛОГИЧЕСКАЯ позиция левого-верхнего угла кадра  */
    int   sx, sy;        /* смещение кадра внутри img (атлас; 0 = целая)   */
    uint8_t w, h;        /* размер кадра (кэш заголовка), 1..255           */
    uint8_t flags;       /* SPR_* | внутренние dirty                       */
    /* --- внутреннее (владеет движок) --- */
    struct {
        int x, y;        /* где нарисован на странице p                    */
        uint8_t on;      /* нарисован ли на странице p                     */
    } drawn[2];
} sprite_t;

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

/* Кадр всей сцены: на ТЕКУЩЕЙ draw-странице (gfx_get_draw_page)
 * heal ВСЕХ старых позиций → блит ВСЕХ видимых (двухпроходно, одна
 * скобка/банк на проход).  Спрайты без изменений с прошлого визита этой
 * страницы (и без SPR_ALWAYS) не трогаются.  Для дабл-буфера звать после
 * gfx_set_draw_page(hidden); для одиночной страницы/курсора — прямо на
 * видимой. */
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);

/* --- 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 void sprite_frame(sprite_t *s, int sx, int sy) {
    if (s->sx != sx || s->sy != sy) { s->sx = sx; s->sy = sy; s->flags |= _SPR_DIRTY; }
}

/* Показать (если был скрыт — грязнит обе страницы). */
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; }

#endif
