/*
 * balls — демо спрайтового движка v2 (<sprite.h>): N шаров 16×16 летают
 * над чёрно-белой шахматкой, отражаясь от краёв.  Esc — выход.
 *
 * РАНЬШЕ (до движка) приложение вело drawn[2][N] координаты вручную и
 * само соблюдало дисциплину «heal ВСЕ → блит ВСЕ» на скрытой странице.
 * Теперь это работа движка: sprite_move меняет только состояние,
 * sprite_flip(sp, N) за кадр делает draw-page=скрытая → heal все старые →
 * блит все видимые (одна W3-скобка + один банк на проход) → vsync → флип.
 * Учёт drawn[2] спрятан в sprite_t.  Профиль/дизайн:
 * docs/sprite-api-design.md §9.1.
 *
 * BOUNDARY-ТЕСТ клипа: диапазон движения выходит ЗА края экрана на
 * половину спрайта — шары штатно наполовину уходят за все 4 края,
 * включая ОТРИЦАТЕЛЬНЫЕ x/y слева/сверху.  Клип _gfx_blit_clip (общий
 * для putsprite и движка) обязан авто-обрезать: правый/нижний — урезать
 * w/h, левый/верхний — сдвинуть начало источника (sx/sy) и урезать.
 *
 * Двойная буферизация: рисуем на скрытой странице, флип по vsync (иначе
 * heal+блит на видимой ловятся лучом — мерцание).  Палитру страницы 1
 * зеркалим из 0 (initgraph грузит EGA только в палитру 0).
 *
 * Скорость привязана к КАДРАМ (50 Гц): один проход цикла = один шаг.
 */

#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include <conio.h>
#include <time.h>

#define FPS_METER 1     /* 1 = счётчик кадров/с слева-сверху (диагностика) */

#define NBALLS  16      /* количество шаров            */
#define BW      16      /* сторона спрайта             */
#define CELL    16      /* клетка шахматки             */

/* Диапазон отражения — на пол-спрайта ЗА каждым краем (boundary-тест):
 * x ∈ [-8 .. 312], y ∈ [-8 .. 248]. */
#define MINY    (0)     /* -8: половина за левым/верхним краем */
#define MINX    (0)     /* -8: половина за левым/верхним краем */
#define MAXX    (320 - BW)  /* 312: половина за правым краем        */
#define MAXY    (256 - BW)  /* 248: половина за нижним краем         */

/* Цвета шаров (не совпадают с фоном BLACK/WHITE) */
static const uint8_t ball_colors[] = {
    YELLOW, LIGHTRED, LIGHTGREEN, LIGHTCYAN,
    LIGHTMAGENTA, LIGHTBLUE, RED, GREEN
};
#define NCOLORS ((uint8_t)sizeof(ball_colors))

static sprite_t sp[NBALLS];                 /* спрайты — движок ведёт drawn[2] */
static int8_t   vx[NBALLS], vy[NBALLS];     /* направление ±1 (физика — наша)  */
static uint8_t  spr[NCOLORS][4 + BW * BW];  /* по спрайту на цвет              */
static uint8_t  egapal[16 * 4];

static void build_sprites(void)
{
    uint8_t c;
    int x, y;

    for (c = 0; c < NCOLORS; c++) {
        uint8_t *p = spr[c];

        *p++ = BW; *p++ = 0; *p++ = BW; *p++ = 0;
        for (y = 0; y < BW; y++)
            for (x = 0; x < BW; x++) {
                int dx = 2 * x - (BW - 1), dy = 2 * y - (BW - 1);  /* ×2: центр между пикселями */
                *p++ = (dx * dx + dy * dy <= 15 * 15)
                       ? ball_colors[c] : GFX_TRANSPARENT;
            }
    }
}

static void draw_board(void)
{
    int cx, cy;

    for (cy = 0; cy < 256; cy += CELL)
        for (cx = 0; cx < 320; cx += CELL) {
            setfillstyle(SOLID_FILL,
                         (((cx ^ cy) >> 4) & 1) ? WHITE : BLACK);
            bar(cx, cy, cx + CELL - 1, cy + CELL - 1);
        }
}

static void init_balls(void)
{
    uint8_t i;

    for (i = 0; i < NBALLS; i++) {
        sprite_init(&sp[i], spr[i % NCOLORS]);
        sp[i].x = (20 + i * 37) % (MAXX - 1) + 1;
        sp[i].y = (20 + i * 53) % (MAXY - 1) + 1;
        sprite_show(&sp[i]);
        vx[i] = (i & 1) ? 1 : -1;
        vy[i] = (i & 2) ? 1 : -1;
    }
}

#if FPS_METER
/* Кадров за последнюю RTC-секунду — белым в чёрной плашке слева-сверху
 * на ТЕКУЩЕЙ draw-странице (рисуем ДО sprite_update, чтобы шары легли
 * поверх и heal учитывал плашку через ОЗУ-копию). */
static void show_fps(uint8_t v)
{
    char buf[4];

    buf[0] = (char)('0' + (v / 100) % 10);
    buf[1] = (char)('0' + (v / 10) % 10);
    buf[2] = (char)('0' + v % 10);
    buf[3] = 0;
    setcolor(WHITE);
    setfillstyle(SOLID_FILL, BLACK);
    bar(0, 0, 27, 9);
    outtextxy(2, 1, buf);
}
#endif

int main(void)
{
    uint8_t i, page, hidden;
#if FPS_METER
    datetime_t dt;
    uint8_t frames = 0, last_sec;
#endif

    build_sprites();
    initgraph();
    gfx_sprite_clip(0);

    /* Палитра страницы 1 = палитре страницы 0. */
    gfx_pal_get(0, 0, 16, egapal);
    gfx_pal_load(1, 0, 16, egapal);

    init_balls();

    /* Обе страницы: шахматка (в ОЗУ-копию — бэкап для heal) + шары в
     * стартовых позициях (sprite_update рисует на текущей draw-странице
     * и снимает её dirty-бит). */
    for (page = 0; page < 2; page++) {
        gfx_set_draw_page(page);
        draw_board();
        sprite_update(sp, NBALLS);
    }
    gfx_set_visible_page(0);
#if FPS_METER
    getdatetime(&dt);
    last_sec = dt.second;
#endif

    for (;;) {
        if (kbhit() && getch() == 27)
            break;

        /* Физика: обновить позиции (только состояние спрайтов). */
        for (i = 0; i < NBALLS; i++) {
            int nx = sp[i].x + vx[i];
            int ny = sp[i].y + vy[i];
            if (nx <= MINX || nx >= MAXX) vx[i] = -vx[i];
            if (ny <= MINY || ny >= MAXY) vy[i] = -vy[i];
            sprite_move(&sp[i], nx, ny);
        }

        /* Кадр: рисуем на скрытой странице.  sprite_flip развёрнут, чтобы
         * плашку FPS нарисовать на скрытой странице ДО sprite_update. */
        hidden = gfx_get_visible_page() ^ 1;
        gfx_set_draw_page(hidden);
#if FPS_METER
        frames++;
        getdatetime(&dt);
        if (dt.second != last_sec) {
            last_sec = dt.second;
            show_fps(frames);
            frames = 0;
        }
#endif
        sprite_update(sp, NBALLS);
        gfx_wait_vsync();
        gfx_set_visible_page(hidden);   /* флип в vblank — без мерцания */
    }

    closegraph();
    return 0;
}
