/*
 * gfx.h — Sprinter graphics: mode-agnostic API (BGI_GFX).
 *
 * Рисование (putpixel/line/bar/circle/…) вынесено в BGI — см.
 * <graphics.h> и driver-библиотеки bgi256.lib / bgi16.lib (выбор режима
 * линковкой: sprinter-cc --gfx 256 | --gfx 16).  Здесь остались только
 * функции БЕЗ BGI-аналога: вход/выход в режим, страницы, банк, vsync,
 * палитра, шрифт.  Они mode-agnostic и живут в libbgi/common/ (один
 * исходник, .rel в обеих driver-библиотеках).
 *
 * Адресация (для не-BGI программ, использующих эти примитивы напрямую: 
 * пиксель (x,y) — CPU 0xC000 + (x или x/2) с Port_Y (0x89) = y; 
 * gfx-код мапит 16 КБ VRAM-страницу в W3 при записи.
 * Для double-buffering страница 1 начинается на 320 байт дальше (0xC140).
 *
 * Палитра: BIOS $A4 (RST 8); 4 байта на запись — B, G, R, pad.
 */
#ifndef GFX_H
#define GFX_H

#include <stdint.h>

/* ESTEX SETVMOD codes — same values as the SETVMOD `A` register. */
#define GFX_MODE_TEXT_40x32       0x02
#define GFX_MODE_TEXT_80x32       0x03
#define GFX_MODE_320x256x256      0x81
#define GFX_MODE_640x256x16       0x82

/* Pixel dimensions of mode 0x81 (320×256, 256 colours). */
#define GFX_WIDTH   320
#define GFX_HEIGHT  256

/* Pixel dimensions of mode 0x82 (640×256, 16 colours).  Each byte at
 * 0xC000+x_byte holds two pixels: high nibble = LEFT (even-x), low
 * nibble = RIGHT (odd-x) — see memory/sprinter_graphics.md. */
#define GFX_WIDTH_16   640
#define GFX_HEIGHT_16  256
#define GFX_COLORS_16  16

/* ---- Page selection (double-buffering) and bank control ---------- */
/* Внутреннее состояние (хранилище: libbgi/common/_gfx_state.c) — НЕ
 * трогать напрямую, только через функции ниже. */
extern uint8_t _gfx_visible_page, _gfx_draw_page, _gfx_bank;

void    gfx_set_visible_page(uint8_t page);   /* 0 or 1 */
inline uint8_t gfx_get_visible_page(void) { return _gfx_visible_page; }
void    gfx_set_draw_page(uint8_t page);      /* 0 or 1 */
inline uint8_t gfx_get_draw_page(void) { return _gfx_draw_page; }

/* Видеобанк W3 (0x50..0x5F): биты 2/3 номера страницы — аппаратные
 * подрежимы ЗАПИСИ (Иван Мак §4.3; docs/sprite-api-design.md §1),
 * действуют на ВСЕ примитивы рисования до следующего gfx_set_bank.
 * Биты 0/1 всегда 0 (резерв конфигураций). */
#define GFX_BANK_NORMAL      0x50  /* запись в видео-ОЗУ + ОЗУ-копию   */
#define GFX_BANK_NOSHADOW    0x54  /* только видео-ОЗУ (врем. вывод)   */
#define GFX_BANK_TRANSPARENT 0x58  /* байт 0xFF не записывается        */
#define GFX_BANK_SPRITE      0x5C  /* NOSHADOW + TRANSPARENT           */

/* Прозрачный цвет аппаратной прозрачности (256-режим: пиксель;
 * 16-режим: ПАРА пикселей цвета 15).  Цвет 255 нельзя нарисовать
 * через банк с битом 3 — он и есть прозрачный. */
#define GFX_TRANSPARENT      0xFF

inline void gfx_set_bank(uint8_t bank) { _gfx_bank = bank; } /* GFX_BANK_* */
inline uint8_t gfx_get_bank(void) { return _gfx_bank; }

/* Клип спрайтов по экрану (putsprite/movesprite/спрайтовый движок).
 * on=1 (дефолт) — клипить (спрайты могут уходить за края); on=0 —
 * ОТКЛЮЧИТЬ (приложение ГАРАНТИРУЕТ, что спрайты целиком на экране —
 * иначе запись за край портит соседнюю память, см. tests/spriteclip).
 * Выигрыш ~+19% на анимации: движок зовёт noclip-ядра без клип-кода
 * (не просто скип проверки — вся клип-математика физически отсутствует,
 * SDCC тесней раскладывает регистры).  Переключение — перенаправлением
 * внутренних указателей на ядра (один раз здесь, не ветка в горячем
 * цикле движка).  НЕ влияет на putimage/gfx_blit (у них клип всегда). */
extern uint8_t _gfx_sprite_clip;
void gfx_sprite_clip(uint8_t on);
inline uint8_t gfx_get_sprite_clip(void) { return _gfx_sprite_clip; }

/* ---- Блиттинг (ядро спрайтов; Фаза B sprite-api-design) ----------- *
 * Формат картинки — getimage: uint16 w, uint16 h, пиксели построчно
 * (1 байт/пиксель в 256-режиме).  Буфер обязан лежать ВНЕ W3
 * (< 0xC000) — на время операции W3 замаплен на видеобанк.
 * Блит через accel block-copy (до 256 байт/burst), клиппинг по экрану
 * есть всегда; рисует ТЕКУЩИМ банком (gfx_set_bank). */

/* Блит целой картинки левым-верхним углом в (x,y). */
void gfx_blit(int x, int y, const void *img);

/* Быстрый блит БЕЗ клипа в ТЕКУЩЕМ банке: картинка обязана быть целиком на
 * экране, w/h ≤ 255.  Идёт линейным спрайтовым ядром — ~2.9× быстрее
 * gfx_blit (замер PoP roomtest 2026-07-27: 4617 против 13288 тактов на
 * спрайт 32×3; общее ядро тратит время на клип/16-бит/split, а не на
 * пиксели).  Для тайловых движков; где нужен клип — gfx_blit/gfx_blit_part. */
void gfx_blit_noclip(int x, int y, const void *img);

/* Быстрый heal БЕЗ клипа: прямоугольник обязан быть целиком на экране,
 * w,h ≤ 255.  Пара к gfx_blit_noclip (общее ядро тратит время на клип и
 * 16-бит, а не на пиксели: замер — 11 658 тактов на heal 22×22). */
void gfx_heal_noclip(int x, int y, uint8_t w, uint8_t h);

/* Блит под-прямоугольника (sx,sy,w,h) картинки (атлас кадров:
 * кадр N ленты — sx = N*FRAME_W).  Под-прямоугольник обязан лежать
 * внутри img; по экрану — клиппится. */
void gfx_blit_part(int x, int y, const void *img,
                   int sx, int sy, int w, int h);

/* Тот же под-прямоугольник БЕЗ клипа по экрану — линейным спрайтовым
 * ядром (пара к gfx_blit_noclip).  Вызывающий гарантирует: под-
 * прямоугольник внутри img, назначение целиком на экране, w,h,y <= 255.
 * Для тайловых движков, кладущих поверх спрайта только перекрывающую его
 * часть тайла (PoP: fore-слой). */
void gfx_blit_part_noclip(int x, int y, const void *img,
                          uint8_t sx, uint8_t sy, uint8_t w, uint8_t h);

/* Блит спрайта column-major (пиксели по колонкам) — вертикальным accel-
 * проходом.  flip!=0 = горизонтальное зеркало (направление персонажа) без
 * CPU-реверса/второй копии; зеркало ВНУТРИ футпринта [x,x+w) (для «hot-
 * point» подавай x=anchor-w при флипе).  Клип по экрану есть.
 * Для персонажей (флип); фон — обычный gfx_blit (row-major). */
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip);

/* То же с ОБРЕЗКОЙ СВЕРХУ: рисуются только строки [skip, skip+rows) кадра,
 * (x,y) — экранная позиция ПЕРВОЙ нарисованной строки (как gfx_blit_part).
 * rows <= 0 = «до низа кадра».  Для персонажа, ушедшего головой под пол
 * (порт clip_char/obj_clip_top в PoP).  Обрезка сверху бесплатна: колонка —
 * непрерывный кусок ОЗУ, сдвигается только старт. */
void gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
                        int skip, int rows);

/* То же + ОГРАНИЧЕНИЕ ШИРИНЫ: рисуются только первые maxw колонок футпринта,
 * считая от x (maxw = 0 — вся ширина кадра).  То есть обрезка СПРАВА по
 * произвольной линии, а не только по краю экрана.
 *
 * Для column-major это столь же дёшево, как обрезка сверху: колонок просто
 * меньше.  Работает и при flip — зеркало происходит внутри футпринта, а
 * первые maxw нарисованных колонок всегда ложатся в его ЛЕВУЮ часть.
 *
 * Мотивация — clip_char в PoP (obj_clip_right): персонаж, поднимающийся по
 * лестнице за дверью уровня, обязан уходить за правый косяк проёма, а сама
 * дверь рисуется ПОД ним и перекрыть его не может. */
void gfx_blit_cols_part_w(int x, int y, const void *img, uint8_t flip,
                          int skip, int rows, uint8_t maxw);

/* То же БЕЗ клипа по экрану — пара к gfx_blit_cols_part так же, как
 * gfx_blit_part_noclip к gfx_blit_part.  Вызывающий гарантирует: спрайт
 * целиком на экране, ширина/высота кадра и y <= 255, skip < высоты.
 * rows = 0 — «до низа кадра».  Экономит ~5.6 К тактов подготовки на вызов
 * (замер PoP roomtest: подготовка 5 622 против 13 596 на сам accel-проход),
 * что для персонажа со спрайтом-компаньоном (клинок) даёт заметную долю
 * кадра. */
void gfx_blit_cols_part_noclip(int x, int y, const void *img, uint8_t flip,
                               uint8_t skip, uint8_t rows);

/* Восстановить прямоугольник экрана из ОЗУ-копии (стирание спрайта/
 * оверлея, нарисованного банком с битом 2: 0x54/0x5C).  Всегда
 * работает банком GFX_BANK_NORMAL независимо от gfx_set_bank;
 * клиппится по экрану.  Буфер сохранения не нужен: ОЗУ-копия и есть
 * бэкап фона (чтение из #50..#5F всегда возвращает её). */
void gfx_heal(int x, int y, int w, int h);

/* Block until the next frame (50 Hz).  Типичный double-buffer:
 *   gfx_set_draw_page(hidden); draw_frame(); gfx_wait_vsync();
 *   gfx_set_visible_page(hidden);   // tear-free flip */
void    gfx_wait_vsync(void);

/* Что вызывать, ПОКА gfx_wait_vsync ждёт луч.  Ожидание кадра — самый
 * длинный простой в кадре (при пейсинге «3 растровых кадра на логический
 * тик» это ~42 мс из 60), и хук занимает его, не отнимая тактов у
 * полезной работы.  Повод — вычерпывание FIFO клавиатуры по опросу
 * (`kbd_raw_poll`, см. <kbd_raw.h>): трёхбайтовый приёмник переполняется
 * на пачках «fake shift», а плотность нужна ~раз в 0.5 мс, которую иначе
 * взять негде.  Хук зовётся в тесном цикле: он обязан быть дешёвым, НЕ
 * рисовать (окно W3 сейчас не за графикой) и не ждать сам.  fn = 0 —
 * снять.  На путь FPS-делителя (gfx_set_fps_div n>=2) не влияет: там
 * ожидание идёт через HALT, и процессор не крутит цикл. */
void    gfx_set_idle_hook(void (*fn)(void));

/* FPS-делитель (frame pacing): логический кадр = РОВНО n кадровых
 * интервалов, независимо от плавания длительности рендера.  n: 1 = 50 fps
 * (дефолт), 2 = 25, 3 = ~16.7, 4 = 12.5, ...; 0 трактуется как 1.
 * Меняет ТОЛЬКО поведение gfx_wait_vsync() (call-sites не трогаются):
 * при n>=2 vsync ждёт n фронтов по фоновому счётчику кадров (ISR),
 * выравниваясь на ближайший фронт при переполнении слота (без дрейфа);
 * при n<=1 — обычный лучевой поллинг.
 * Возврат 0 / -1+errno: EINVAL (неподходящий memory mode — данные не в
 * W2), ENOMEM (заняты все слоты цепочки кадровых обработчиков). */
int     gfx_set_fps_div(uint8_t n);

/* ---- Bitmap-font text -------------------------------------------- *
 * Шрифт 256 глифов × 8 рядов × 1 байт (ZX-Spectrum формат), 2 КБ.
 * Грузится лениво при первом использовании BGI-текста; gfx_set_font
 * подменяет указатель (storage должен жить, пока шрифт активен). */
void    gfx_load_default_font(void);
void    gfx_set_font(const uint8_t *font);

/* ---- Palette ----------------------------------------------------- *
 * Каждая графическая страница имеет свою палитру (page 0 → palette 0,
 * page 1 → palette 1).  Для seamless double-buffering грузить одну
 * палитру в обе. */
void    gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
                     const uint8_t *data);
void    gfx_pal_set (uint8_t pal_num, uint8_t idx,
                     uint8_t r, uint8_t g, uint8_t b);
void    gfx_pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
                     uint8_t *data);
void    gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
                          uint8_t *r, uint8_t *g, uint8_t *b);
void    gfx_pal_reset(void);

/* Палитра страницы 1 := палитре страницы 0 (все 256 записей).
 * ОБЯЗАТЕЛЬНЫЙ шаг дабл-буфера: без него флип на страницу 1 показывает
 * сцену чёрной (у каждой страницы своя палитра).  Звать после initgraph
 * и после правок палитры 0. */
void    gfx_pal_sync(void);

/* Палитра ↔ файл.  Формат: записи по 4 байта (B, G, R, 0 — родной
 * BIOS $A4), полная палитра = 1024 байта.  fload принимает и усечённый
 * файл (напр. 64 Б = 16 цветов) — остальные слоты не трогает; возврат:
 * число загруженных записей или -1.  fsave пишет все 256; 0 / -1. */
int     gfx_pal_fsave(uint8_t pal_num, const char *path);
int     gfx_pal_fload(uint8_t pal_num, const char *path);

#endif
