//-------------------------------------------------------------------------
//
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
//-------------------------------------------------------------------------
//
// scroll_test_tiga.c
//
// Renders page text using TIGA bitmap fonts (FONTnn.tiga fill glyphs +
// SHDTnn.tiga shadow/border glyphs) into a 768-px-wide internal buffer,
// horizontally resamples to 720 px (Catmull-Rom bicubic), and blits the
// result to the dispmanx 'over' layer.
//
// JSON page semantics handled here:
//   height 0/1/2  -> FONT01 (20px line) / FONT02 (40px) / FONT07 (80px)
//   width  0/1/.. -> horizontal pixel multiplier (0 = 1x, 1 = 2x, ...)
//   border        -> SHDTnn shadow glyphs drawn underneath the fill glyphs
//   color         -> per-line text color (see text_palette[])
//   roll          -> top region scrolls vertically (seamless wrap)
//   chain         -> content of the next logical page is appended to the
//                    scroll content of this page (followed transitively)
//
// Font files expected in the working directory:
//   FONT01.tiga FONT02.tiga FONT07.tiga
//   SHDT01.tiga SHDT02.tiga SHDT07.tiga
// (Other FONTnn/SHDTnn load lazily if present; missing fonts fall back.)
//
//-------------------------------------------------------------------------

#define _GNU_SOURCE

#include <assert.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pthread.h>
#include <time.h>

#ifdef MWINDOW
//-------------------------------------------------------------------------
// MWINDOW build: Win32 preview window (MinGW).  No raspidmx, no dispmanx.
// Build:
//   x86_64-w64-mingw32-gcc -O2 -DMWINDOW scroll_test_tiga.c
//       -o scroll_preview.exe -lgdi32 -lws2_32 -lpthread -lm
//   (single command line; wrapped here for readability)
//-------------------------------------------------------------------------
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#include "jsmn.h"

#define sockread(s,b,l)  recv((s),(b),(int)(l),0)
#define sockclose(s)     closesocket(s)
typedef SOCKET sock_t;
#ifndef SO_REUSEPORT
#define SO_REUSEPORT 0   /* not a thing on Windows; OR'ing 0 is harmless */
#endif

//---- raspidmx type/function shims --------------------------------------

typedef struct { uint8_t red, green, blue, alpha; } RGBA8_T;
typedef struct { void *buffer; int width, height; } IMAGE_T;
typedef struct { IMAGE_T image; } IMAGE_LAYER_T;

static void clearImageRGB(IMAGE_T *img, const RGBA8_T *c)
{
    uint32_t v = ((uint32_t)c->red)         |
                 ((uint32_t)c->green << 8)  |
                 ((uint32_t)c->blue  << 16) |
                 ((uint32_t)c->alpha << 24);
    uint32_t *p = (uint32_t *)img->buffer;
    for (int i = 0; i < img->width * img->height; i++) p[i] = v;
}

// Inclusive box fill, clamped to image bounds (matches raspidmx usage)
static void imageBoxFilledRGB(IMAGE_T *img, int x1, int y1, int x2, int y2,
                              const RGBA8_T *c)
{
    uint32_t v = ((uint32_t)c->red)         |
                 ((uint32_t)c->green << 8)  |
                 ((uint32_t)c->blue  << 16) |
                 ((uint32_t)c->alpha << 24);
    if (x1 < 0) x1 = 0;
    if (y1 < 0) y1 = 0;
    if (x2 >= img->width)  x2 = img->width  - 1;
    if (y2 >= img->height) y2 = img->height - 1;
    uint32_t *p = (uint32_t *)img->buffer;
    for (int y = y1; y <= y2; y++)
        for (int x = x1; x <= x2; x++)
            p[y * img->width + x] = v;
}

// Layer push is a no-op in the preview build — the window is repainted
// from the composed buffers once per frame in winPresent().
static void changeSourceAndUpdateImageLayer(IMAGE_LAYER_T *l) { (void)l; }

// Keyboard comes from the window message pump instead of the console
static int  keyPressed(int *k) { (void)k; return 0; }
static void keyboardReset(void) {}

#else
//-------------------------------------------------------------------------
// Pi build: raspidmx + dispmanx (unchanged behavior)
//-------------------------------------------------------------------------
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>

#include "backgroundLayer.h"
#include "image.h"
#include "imageLayer.h"
#include "imageGraphics.h"
#include "loadpng.h"
#include "jsmn.h"
#include "key.h"

#include "bcm_host.h"

#define sockread(s,b,l)  read((s),(b),(l))
#define sockclose(s)     close(s)
typedef int sock_t;

#endif // MWINDOW

#define PORT 6867

//-------------------------------------------------------------------------
// Layout constants
//-------------------------------------------------------------------------

// Internal render width.  All text is rasterized at this width, then
// horizontally resampled down to DISP_W before display.
#define RENDER_W     768

// Final display dimensions
#define DISP_W       720
#define DISP_H       480

// Top region: rows 0..TOP_H-1   (page content)
#define TOP_H        380
// Bottom region: rows BOT_Y..479 (LDL: clock / crawl lines)
#define BOT_Y        380
#define BOT_H        100

// First text row offset within the top region (matches original layout)
#define TOP_TEXT_Y   40

// Scroll buffer height (768-wide render side).  Must hold 2x the largest
// chained content block.  9 lines x 80px = 720px/page; this allows ~12
// chained 80px pages doubled.  Tune down if memory matters.
#define SCROLL_BUF_H 4096
// Cap on a single content copy (half the buffer, so x2 render fits)
#define CONTENT_MAX_H (SCROLL_BUF_H / 2)

// Max pages a chain is allowed to follow (loop guard)
#define CHAIN_MAX    8

// Pixels per frame the top region scrolls upward when roll is active.
// At ~60fps: 1 = 60 px/s, 2 = 120 px/s.
#define SCROLL_SPD   1

//-------------------------------------------------------------------------
// Pixel packing for VC_IMAGE_RGBA32 (matches byte order used previously)
//-------------------------------------------------------------------------

#define PACK_RGBA(r,g,b,a) ( ((uint32_t)(r))        | \
                             ((uint32_t)(g) <<  8)  | \
                             ((uint32_t)(b) << 16)  | \
                             ((uint32_t)(a) << 24) )

//-------------------------------------------------------------------------
// Globals carried over from the original program
//-------------------------------------------------------------------------

int	s_width;
int	s_height;
int	margin = 15;

int	horizalign = 71;   // left margin for text, in RENDER_W (768) space
int	valign = 1;

char line[512] = "";
char ldl[12][64];

char pagestore[4096][64];
// pagestore, pages start each 16 rows (page 1 @ 16, page 2 = 32, ...)
//
// header properties located at first row
// byte 0: page number
// byte 1: line count
// byte 2: spare, freeze, advise, warn
// byte 3: spare, flip, roll, chain
//
// byte 4: line 1 - separator, flash, reverse, border
// byte 5: line 1 - color
// repeat for each line
//
// line properties
// byte 34: row number
// byte 35: height/width   (height in bits 2-3, width in bits 0-1)
// repeat for each line
//

char omcw[8];
// omcw
// byte 1: spare, regSep, botSolid, topSolid (bitwise)
// byte 2: top Page
// byte 3: bottom Page
//

volatile bool run = true;

IMAGE_LAYER_T		solid;
IMAGE_LAYER_T		over;

RGBA8_T clear_color  = {  0,   0,   0,   0};

RGBA8_T star_white   = {215, 215, 215, 255};
RGBA8_T star_black   = {  0,   0,   0, 255};
RGBA8_T star_gray    = { 98,  98,  98, 255};
RGBA8_T star_red     = { 96,   0,   0, 255};
RGBA8_T star_green   = {  0, 129,   0, 255};
RGBA8_T star_blue    = {  0,   0,  96, 255};
RGBA8_T star_purple  = { 66,  11, 149, 255};

//-------------------------------------------------------------------------
// Text color palette — indexed by the per-line "color" field (0..15).
// Tune freely; unset entries default to white.
//-------------------------------------------------------------------------

static const uint32_t text_palette[16] = {
    PACK_RGBA(255, 255, 255, 255),   //  0  white
    PACK_RGBA(255,  80,  80, 255),   //  1  red
    PACK_RGBA( 80, 255,  80, 255),   //  2  green
    PACK_RGBA(255, 255,  80, 255),   //  3  yellow
    PACK_RGBA(100, 140, 255, 255),   //  4  blue
    PACK_RGBA(255, 255, 255, 255),   //  5  white (cc/tcf default)
    PACK_RGBA(255, 160,  60, 255),   //  6  orange
    PACK_RGBA(180, 120, 255, 255),   //  7  violet
    PACK_RGBA(255, 255, 255, 255),   //  8
    PACK_RGBA(255, 255, 255, 255),   //  9
    PACK_RGBA(255, 255, 255, 255),   // 10
    PACK_RGBA(255, 255, 255, 255),   // 11
    PACK_RGBA(255, 255, 255, 255),   // 12
    PACK_RGBA(255, 255, 255, 255),   // 13
    PACK_RGBA(255, 255, 255, 255),   // 14
    PACK_RGBA(255, 255, 255, 255),   // 15
};

// Shadow / border glyph color (drawn underneath the fill glyphs)
static const uint32_t shadow_color = PACK_RGBA(0, 0, 0, 255);

//-------------------------------------------------------------------------
// TIGA font support
//-------------------------------------------------------------------------
//
// File format (little-endian; offsets in bytes unless noted):
//   0   2  magic 0x40 0x80
//   2   4  u32 file length
//   6  30  face name (NUL padded)
//  36   2  u16 default char
//  38   2  u16 first char
//  40   2  u16 last char
//  42   2  u16 max width
//  44   2  u16 max kern
//  46   2  u16 char width (0 = proportional)
//  48   2  u16 avg width
//  50   2  u16 char height
//  52   2  u16 ascent
//  54   2  u16 descent
//  56   2  u16 leading
//  58   4  u32 row pitch (pixel columns across the whole sheet)
//  62   4  u32 pattern table offset  (in BITS from file start)
//  66   4  u32 location table offset (in BITS)
//  70   4  u32 width table offset    (in BITS)
//
// Pattern table: charheight rows x rowpitch bits, one continuous
// LSB-first bit stream.
// Location table: (last-first+2) u16 entries — pixel column where each
// glyph starts; entry i+1 minus entry i gives glyph i's width.
// Width table: (last-first+2) byte pairs [cell_width, glyph_offset].
//
//-------------------------------------------------------------------------

typedef struct {
    bool      loaded;
    char      facename[31];
    int       first, last, deflt;
    int       charheight;
    int       leading;
    int       rowpitch;
    int       num_chars;       // (last - first) + 2 (includes sentinel)
    uint8_t  *bitmap;          // rowpitch * charheight, 1 byte per pixel (0/1)
    uint16_t *location;        // num_chars entries
    uint8_t  *cell_width;      // num_chars entries
    uint8_t  *glyph_offset;    // num_chars entries
} TIGA_FONT;

// fonts[n] / shadow_fonts[n] correspond to FONT0n.tiga / SHDT0n.tiga
static TIGA_FONT fonts[8];
static TIGA_FONT shadow_fonts[8];

static uint16_t rd_u16(const uint8_t *p) { return (uint16_t)(p[0] | (p[1] << 8)); }
static uint32_t rd_u32(const uint8_t *p) {
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
           ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static bool loadTigaFont(const char *path, TIGA_FONT *f)
{
    memset(f, 0, sizeof(*f));

    FILE *fp = fopen(path, "rb");
    if (!fp) return false;

    fseek(fp, 0, SEEK_END);
    long fsize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    if (fsize < 78) { fclose(fp); return false; }

    uint8_t *data = malloc((size_t)fsize);
    if (!data) { fclose(fp); return false; }
    if (fread(data, 1, (size_t)fsize, fp) != (size_t)fsize) {
        free(data); fclose(fp); return false;
    }
    fclose(fp);

    if (data[0] != 0x40 || data[1] != 0x80) {
        fprintf(stderr, "%s: bad TIGA magic\n", path);
        free(data); return false;
    }

    memcpy(f->facename, data + 6, 30);
    f->facename[30] = '\0';

    f->deflt      = rd_u16(data + 36);
    f->first      = rd_u16(data + 38);
    f->last       = rd_u16(data + 40);
    f->charheight = rd_u16(data + 50);
    f->leading    = rd_u16(data + 56);
    f->rowpitch   = (int)rd_u32(data + 58);

    uint32_t pat_bits = rd_u32(data + 62);
    uint32_t loc_bits = rd_u32(data + 66);
    uint32_t wid_bits = rd_u32(data + 70);

    f->num_chars = (f->last - f->first) + 2;

    if (f->charheight <= 0 || f->rowpitch <= 0 || f->num_chars <= 1) {
        free(data); return false;
    }

    //-- pattern table: continuous LSB-first bit stream
    f->bitmap = malloc((size_t)f->rowpitch * f->charheight);
    if (!f->bitmap) { free(data); return false; }

    {
        size_t byte_pos = pat_bits / 8;
        int    bit_pos  = 0;
        for (int y = 0; y < f->charheight; y++) {
            for (int x = 0; x < f->rowpitch; x++) {
                if (bit_pos >= 8) { bit_pos = 0; byte_pos++; }
                if (byte_pos >= (size_t)fsize) {
                    fprintf(stderr, "%s: pattern table overruns file\n", path);
                    free(f->bitmap); f->bitmap = NULL;
                    free(data); return false;
                }
                f->bitmap[y * f->rowpitch + x] =
                    (data[byte_pos] >> bit_pos) & 1;
                bit_pos++;
            }
        }
    }

    //-- location table
    f->location = malloc((size_t)f->num_chars * sizeof(uint16_t));
    //-- width table
    f->cell_width   = malloc((size_t)f->num_chars);
    f->glyph_offset = malloc((size_t)f->num_chars);

    if (!f->location || !f->cell_width || !f->glyph_offset) {
        free(f->bitmap); free(f->location);
        free(f->cell_width); free(f->glyph_offset);
        memset(f, 0, sizeof(*f));
        free(data); return false;
    }

    {
        size_t seek = loc_bits / 8;
        for (int i = 0; i < f->num_chars; i++) {
            f->location[i] = rd_u16(data + seek);
            seek += 2;
        }
    }
    {
        size_t seek = wid_bits / 8;
        for (int i = 0; i < f->num_chars; i++) {
            f->cell_width[i]   = data[seek];       // advance width
            f->glyph_offset[i] = data[seek + 1];   // left-side bearing
            seek += 2;
        }
    }

    free(data);
    f->loaded = true;

    printf("TIGA: %-10s  %s  h=%d lead=%d pitch=%d chars=0x%02X..0x%02X\n",
           path, f->facename, f->charheight, f->leading,
           f->rowpitch, f->first, f->last);
    return true;
}

static void freeTigaFont(TIGA_FONT *f)
{
    if (!f->loaded) return;
    free(f->bitmap);
    free(f->location);
    free(f->cell_width);
    free(f->glyph_offset);
    memset(f, 0, sizeof(*f));
}

//-------------------------------------------------------------------------
// JSON "height" field (0..3) -> font number.
//   height 0 -> FONT01 (18px glyphs, 20px line)
//   height 1 -> FONT02 (36px glyphs, 40px line)
//   height 2 -> FONT07 (72px glyphs, 80px line)
//-------------------------------------------------------------------------

static const int font_for_height[4] = { 1, 2, 7, 7 };

// Line advance for a height value = charheight + leading of its font.
// Falls back to 20*(h+1) if the font is missing.
static int lineAdvanceForHeight(int h)
{
    int fn = font_for_height[h & 3];
    if (fonts[fn].loaded) {
        return fonts[fn].charheight + fonts[fn].leading;
    }
    return 20 * ((h & 3) + 1);
}

//-------------------------------------------------------------------------
// Glyph rasterizer: draws one string with a single font into a flat
// uint32_t buffer.  xscale duplicates pixels horizontally (width field).
// Used twice per line when border is set: shadow font pass, then fill.
//-------------------------------------------------------------------------

static void drawTigaString(uint32_t *buf, int buf_w, int buf_h,
                           int x, int y,
                           const char *str,
                           const TIGA_FONT *f,
                           uint32_t color,
                           int xscale)
{
    if (!f || !f->loaded || xscale < 1) return;

    int pen_x = x;

    for (const char *p = str; *p; p++) {
        int c = (unsigned char)*p;
        if (c < f->first || c > f->last) c = f->deflt;
        int idx = c - f->first;
        if (idx < 0 || idx >= f->num_chars - 1) continue;

        int glyph_x = f->location[idx];
        int glyph_w = f->location[idx + 1] - f->location[idx];
        int offset  = f->glyph_offset[idx];
        int cell_w  = f->cell_width[idx];

        for (int gy = 0; gy < f->charheight; gy++) {
            int dst_y = y + gy;
            if (dst_y < 0 || dst_y >= buf_h) continue;

            const uint8_t *row = f->bitmap + (size_t)gy * f->rowpitch;
            uint32_t *dst_row  = buf + (size_t)dst_y * buf_w;

            for (int gx = 0; gx < glyph_w; gx++) {
                if (!row[glyph_x + gx]) continue;

                int base_x = pen_x + (gx + offset) * xscale;
                for (int s = 0; s < xscale; s++) {
                    int dst_x = base_x + s;
                    if (dst_x >= 0 && dst_x < buf_w) {
                        dst_row[dst_x] = color;
                    }
                }
            }
        }

        pen_x += cell_w * xscale;
    }
}

// Draws one text line with optional shadow underlay.
// height_field selects the font pair; border enables the SHDT pass.
static void drawTextLine(uint32_t *buf, int buf_w, int buf_h,
                         int x, int y,
                         const char *str,
                         int height_field, int width_field,
                         int color_idx, bool border)
{
    int fn     = font_for_height[height_field & 3];
    int xscale = (width_field & 3) + 1;
    uint32_t fg = text_palette[color_idx & 15];

    // Shadow pass first — full string, so a later character's fill is
    // never overdrawn by the next character's shadow.
    if (border && shadow_fonts[fn].loaded) {
        drawTigaString(buf, buf_w, buf_h, x, y, str,
                       &shadow_fonts[fn], shadow_color, xscale);
    }

    if (fonts[fn].loaded) {
        drawTigaString(buf, buf_w, buf_h, x, y, str,
                       &fonts[fn], fg, xscale);
    }
}

//-------------------------------------------------------------------------
// Horizontal bicubic resampler: RENDER_W (768) -> DISP_W (720).
//
// The source-to-destination column mapping never changes, so the 4-tap
// Catmull-Rom weights are precomputed once at startup in 8.8 fixed point.
// Resampling a row is then 4 multiply-accumulates per channel per pixel.
//-------------------------------------------------------------------------

typedef struct {
    int16_t idx[4];   // source columns (clamped)
    int16_t w[4];     // weights, 8.8 fixed point, sum = 256
} HTAP;

static HTAP htaps[DISP_W];

static double cubic_weight(double t)
{
    double at = fabs(t);
    if (at < 1.0) return 1.5*at*at*at - 2.5*at*at + 1.0;
    if (at < 2.0) return -0.5*at*at*at + 2.5*at*at - 4.0*at + 2.0;
    return 0.0;
}

static void initHResize(void)
{
    double scale = (double)RENDER_W / DISP_W;

    for (int dx = 0; dx < DISP_W; dx++) {
        double sx = (dx + 0.5) * scale - 0.5;
        int    ix = (int)floor(sx);

        double wd[4];
        double sum = 0.0;
        for (int n = 0; n < 4; n++) {
            wd[n] = cubic_weight(sx - (ix - 1 + n));
            sum += wd[n];
        }

        int wsum = 0;
        for (int n = 0; n < 4; n++) {
            int col = ix - 1 + n;
            if (col < 0) col = 0;
            if (col >= RENDER_W) col = RENDER_W - 1;
            htaps[dx].idx[n] = (int16_t)col;
            // Normalize so fixed-point weights sum to exactly 256
            htaps[dx].w[n] = (int16_t)lround(wd[n] / sum * 256.0);
            wsum += htaps[dx].w[n];
        }
        // Push any rounding residue into the largest tap
        htaps[dx].w[1] += (int16_t)(256 - wsum);
    }
}

static inline uint8_t clamp_u8(int v)
{
    if (v < 0)   return 0;
    if (v > 255) return 255;
    return (uint8_t)v;
}

// Resample `rows` rows from src (RENDER_W wide) into dst (DISP_W wide).
static void hresize(const uint32_t *src, uint32_t *dst, int rows)
{
    for (int y = 0; y < rows; y++) {
        const uint32_t *s = src + (size_t)y * RENDER_W;
        uint32_t       *d = dst + (size_t)y * DISP_W;

        for (int x = 0; x < DISP_W; x++) {
            const HTAP *t = &htaps[x];
            int r = 0, g = 0, b = 0, a = 0;

            for (int n = 0; n < 4; n++) {
                uint32_t p = s[t->idx[n]];
                int w = t->w[n];
                r += w * (int)( p        & 0xFF);
                g += w * (int)((p >>  8) & 0xFF);
                b += w * (int)((p >> 16) & 0xFF);
                a += w * (int)((p >> 24) & 0xFF);
            }

            d[x] = PACK_RGBA(clamp_u8(r >> 8), clamp_u8(g >> 8),
                             clamp_u8(b >> 8), clamp_u8(a >> 8));
        }
    }
}

//-------------------------------------------------------------------------
// Render buffers
//
// 768-wide "compose" buffers (text is rasterized here):
//   top768    — static top region compose      (RENDER_W x TOP_H)
//   scroll768 — rolling/chained content compose (RENDER_W x SCROLL_BUF_H)
//   bot768    — LDL region compose              (RENDER_W x BOT_H)
//
// 720-wide display-side buffers (post-resize):
//   top_buf    — static top region   (DISP_W x TOP_H)
//   scroll_buf — resized scroll content (DISP_W x SCROLL_BUF_H)
//   bot_buf    — LDL region          (DISP_W x BOT_H)
//
// Per frame the only work is a memcpy of the (sliding) window into
// over.image — rasterization + resize happen only on content changes.
//-------------------------------------------------------------------------

static uint32_t *top768    = NULL;
static uint32_t *scroll768 = NULL;
static uint32_t *bot768    = NULL;

static uint32_t *top_buf    = NULL;
static uint32_t *scroll_buf = NULL;
static uint32_t *bot_buf    = NULL;

static int  scroll_y    = 0;
static bool solid_dirty = true;

static inline void buf_clear(uint32_t *buf, int w, int h)
{
    memset(buf, 0, (size_t)w * h * 4);
}

//-------------------------------------------------------------------------
// Page line helpers
//-------------------------------------------------------------------------

static inline int pageLineCount(int page)
{
    int lc = (unsigned char)pagestore[16 * page][1];
    return (lc > 0 && lc < 16) ? lc : 0;
}

static inline bool pageValid(int page)
{
    return page > 0 && page < 256 &&
           (unsigned char)pagestore[16 * page][0] > 0;
}

static inline int lineHeightField(int page, int i)
{
    return ((unsigned char)pagestore[16 * page][35 + 2*i] & 12) >> 2;
}
static inline int lineWidthField(int page, int i)
{
    return (unsigned char)pagestore[16 * page][35 + 2*i] & 3;
}
static inline int lineColor(int page, int i)
{
    return (unsigned char)pagestore[16 * page][5 + 2*i] & 15;
}
static inline bool lineBorder(int page, int i)
{
    return ((unsigned char)pagestore[16 * page][4 + 2*i] & 1) != 0;
}
static inline bool pageRoll(int page)
{
    return ((unsigned char)pagestore[16 * page][3] & 2) != 0;
}
static inline bool pageChain(int page)
{
    return ((unsigned char)pagestore[16 * page][3] & 1) != 0;
}

//-------------------------------------------------------------------------
// renderPageLines — rasterize all lines of one page into a 768-wide buffer
// starting at y_base.  Returns the pixel height consumed.
//-------------------------------------------------------------------------

static int renderPageLines(int page, uint32_t *buf, int buf_h, int y_base)
{
    int lc = pageLineCount(page);
    int y  = y_base;

    for (int i = 0; i < lc; i++) {
        int h_field = lineHeightField(page, i);
        int w_field = lineWidthField(page, i);
        int advance = lineAdvanceForHeight(h_field);

        if (y + advance > buf_h) break;   // out of room

        drawTextLine(buf, RENDER_W, buf_h,
                     horizalign, y + valign,
                     pagestore[(16 * page) + i + 1],
                     h_field, w_field,
                     lineColor(page, i),
                     lineBorder(page, i));

        y += advance;
    }

    return y - y_base;
}

//-------------------------------------------------------------------------
// renderTopStatic — non-rolling top region: compose at 768, resize to 720.
//-------------------------------------------------------------------------

static void renderTopStatic(int page)
{
    buf_clear(top768, RENDER_W, TOP_H);

    if (pageValid(page) && pageLineCount(page) > 0) {
        renderPageLines(page, top768, TOP_H, TOP_TEXT_Y);
    }

    hresize(top768, top_buf, TOP_H);
}

//-------------------------------------------------------------------------
// buildScrollContent — rolling top region.
//
// Renders page content (following the chain flag through successive
// logical pages) once into scroll768, then renders the whole block a
// second time immediately below it so the sliding window wraps without
// a seam.  Resizes the used portion to 720 into scroll_buf.
//
// content_h_out receives the height of ONE copy of the content.
//-------------------------------------------------------------------------

static void buildScrollContent(int page, int *content_h_out)
{
    buf_clear(scroll768, RENDER_W, SCROLL_BUF_H);

    // Collect the chain: page, then successors while chain flag set.
    int chain_pages[CHAIN_MAX];
    int chain_len = 0;

    int p = page;
    for (int hop = 0; hop < CHAIN_MAX; hop++) {
        if (!pageValid(p) || pageLineCount(p) == 0) break;

        // Loop guard: stop if this page is already in the chain
        bool seen = false;
        for (int q = 0; q < chain_len; q++) {
            if (chain_pages[q] == p) { seen = true; break; }
        }
        if (seen) break;

        chain_pages[chain_len++] = p;

        if (!pageChain(p)) break;
        p = p + 1;   // next logical page
    }

    // First copy: render every chained page, stacked, starting at
    // TOP_TEXT_Y so the first screenful matches the static layout.
    int y = TOP_TEXT_Y;
    for (int q = 0; q < chain_len && y < CONTENT_MAX_H; q++) {
        y += renderPageLines(chain_pages[q], scroll768, CONTENT_MAX_H, y);
    }

    // One copy of the content must be at least TOP_H tall so the window
    // always has a full frame to slide over.
    int content_h = (y > TOP_H) ? y : TOP_H;
    if (content_h > CONTENT_MAX_H) content_h = CONTENT_MAX_H;

    // Second copy: same content rendered at y + content_h for seamless wrap.
    y = content_h + TOP_TEXT_Y;
    for (int q = 0; q < chain_len && y < SCROLL_BUF_H; q++) {
        y += renderPageLines(chain_pages[q], scroll768, SCROLL_BUF_H, y);
    }

    // Resize only the rows actually in use: content_h + one window height.
    int used_rows = content_h + TOP_H;
    if (used_rows > SCROLL_BUF_H) used_rows = SCROLL_BUF_H;
    hresize(scroll768, scroll_buf, used_rows);

    if (content_h_out) *content_h_out = content_h;
}

//-------------------------------------------------------------------------
// renderBot — LDL region (clock/date line + crawl line).
// Re-rendered only when its strings change (tracked by caller).
// Clock uses FONT01 (small), LDL text uses FONT02 — same size roles the
// old PNG charsets had.
//-------------------------------------------------------------------------

static void renderBot(const char *clockstr, int ldli)
{
    buf_clear(bot768, RENDER_W, BOT_H);

    if (omcw[3]) {
        // Clock/date — original drew at display y=383 → local y=3
        drawTextLine(bot768, RENDER_W, BOT_H,
                     horizalign, 3,
                     clockstr,
                     0 /*height: FONT01*/, 0 /*width 1x*/,
                     0 /*white*/, true /*border*/);

        // Crawl line — original y=405 → local y=25
        if (pagestore[800][1] > 0 && pagestore[800][1] < 16) {
            if (ldli > 0 && ldli < 16) {
                drawTextLine(bot768, RENDER_W, BOT_H,
                             horizalign, 25,
                             pagestore[800 + ldli],
                             1 /*height: FONT02*/, 0,
                             0 /*white*/, true /*border*/);
            }
        }
    }

    hresize(bot768, bot_buf, BOT_H);
}

//-------------------------------------------------------------------------
// blitBuffersToOver — copy the (possibly scrolled) top window and the
// bottom region into over.image and push the layer.
//-------------------------------------------------------------------------

static void blitBuffersToOver(bool rolling, int content_h)
{
    uint32_t *dst = (uint32_t *)over.image.buffer;

    if (rolling && content_h > 0) {
        scroll_y = (scroll_y + SCROLL_SPD) % content_h;
        memcpy(dst,
               scroll_buf + (size_t)scroll_y * DISP_W,
               (size_t)DISP_W * TOP_H * 4);
    } else {
        memcpy(dst, top_buf, (size_t)DISP_W * TOP_H * 4);
    }

    memcpy(dst + (size_t)BOT_Y * DISP_W,
           bot_buf,
           (size_t)DISP_W * BOT_H * 4);

    changeSourceAndUpdateImageLayer(&over);
}

//-------------------------------------------------------------------------
// JSON helpers
//-------------------------------------------------------------------------

static int jsoneq(const char *json, jsmntok_t *tok, const char *s) {
    if (tok->type == JSMN_STRING && (int)strlen(s) == tok->end - tok->start && strncmp(json + tok->start, s, tok->end - tok->start) == 0) {
        return 0;
    }
    return -1;
}

//-------------------------------------------------------------------------
// TCP / JSON socket (logic unchanged from original; solid_dirty and
// scroll resets added where state changes)
//-------------------------------------------------------------------------

void createSock() {

	sock_t server_fd, new_socket;
	int valread;
	struct sockaddr_in address;
	int opt = 1;
	int addrlen = sizeof(address);
	char buffer[4096] = {0};
	char *hello = "Hello from server";

	if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == (sock_t)-1) {
		perror ("socket failed");
		exit(EXIT_FAILURE);
	}

	if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
	               (const char *)&opt, sizeof(opt))) {
		perror("setsockopt");
		exit(EXIT_FAILURE);
	}

	address.sin_family = AF_INET;
	address.sin_addr.s_addr = INADDR_ANY;
	address.sin_port = htons( PORT );

	if (bind(server_fd, (struct sockaddr *) &address, sizeof (address)) <0) {
		perror("bind failed");
		exit(EXIT_FAILURE);
	}

	if (listen(server_fd, 3) < 0) {
		perror("listen");
		exit(EXIT_FAILURE);
	}
	if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) == (sock_t)-1) {
		perror("accept");
		exit(EXIT_FAILURE);
	}

	// Receive "NNN#<payload>" where NNN = decimal payload byte count.
	// Accumulates directly into buffer[]; tolerates the header being
	// split across reads, stops on client close/error, and never scans
	// past what was actually received.
	//
	// total_expected = header bytes + payload bytes, parsed once the
	// '#' terminator has arrived.  -1 until then.
	int valoffset      = 0;
	int total_expected = -1;
	const int buf_cap  = (int)sizeof(buffer) - 1;   // reserve NUL

	while (valoffset < buf_cap) {
		char scb[4096];
		valread = sockread(new_socket, scb, sizeof(scb));

		if (valread <= 0) {
			// Client closed (0) or socket error (-1): stop with
			// whatever arrived rather than spinning forever.
			if (valread < 0) perror("read");
			break;
		}

		// Append, clamped to capacity
		int space = buf_cap - valoffset;
		int ncopy = (valread < space) ? valread : space;
		memcpy(buffer + valoffset, scb, ncopy);
		valoffset += ncopy;

		// Parse the "NNN#" header once it's fully present
		if (total_expected < 0) {
			for (int z = 0; z < valoffset; z++) {
				if (buffer[z] == '#') {
					int payload_len = 0;
					for (int q = 0; q < z; q++) {
						if (buffer[q] >= '0' && buffer[q] <= '9') {
							payload_len = (payload_len * 10) + (buffer[q] - '0');
						}
					}
					total_expected = (z + 1) + payload_len;
					if (total_expected > buf_cap) {
						fprintf(stderr,
							"warning: declared length %d exceeds buffer; truncating\n",
							payload_len);
						total_expected = buf_cap;
					}
					break;
				}
			}
		}

		printf("expected: %d  received: %d\n", total_expected, valoffset);

		// Done once the full declared message has arrived
		if (total_expected >= 0 && valoffset >= total_expected) {
			break;
		}
	}
	buffer[valoffset] = '\0';
	printf( "%s\nsize: %i\n\n", buffer, valoffset);

	// -- json part

	// Blank out everything before the first '{' (bounded; if no JSON
	// arrived at all, skip parsing and just close the connection)
	{
		int j = 0;
		while (j < valoffset && buffer[j] != '{') {
			buffer[j] = ' ';
			j++;
		}
		if (j >= valoffset) {
			fprintf(stderr, "no JSON object in received data\n");
			send(new_socket, hello, strlen(hello), 0);
			sockclose(new_socket);
			sockclose(server_fd);
			return;
		}
	}

	int r;

	jsmn_parser p;
	jsmntok_t t[256] = {0};

	jsmn_init(&p);
	r = jsmn_parse(&p, buffer, strlen(buffer), t, 256);

	char stx[128];

	// == OMCW JSON

	char sub[24] = "pageData";
	char scratch[16][64];

	int lgp0[9] = {0};
	// lgp0[x] = line counter for decoded json
	// 0: separator  1: flash  2: reverse  3: border
	// 4: color  5: rowNumber  6: height  7: width  8: textData

	for(int q = 0; q < 16; q++) {
		for(int qq = 0; qq < (int)sizeof(scratch[q]); qq++) {
			scratch[q][qq] = 0;
		}
	}

	for (int i = 1; i < r; i++) {
		if (jsoneq(buffer, &t[i], "bottomSolid") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int mt = strcmp(stx, "true");
			if (mt == 0) {
				omcw[1] = (omcw[1] | 1);
			} else {
				omcw[1] = (omcw[1] & ~1);
			}
			solid_dirty = true;
		}
		if (jsoneq(buffer, &t[i], "topSolid") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int mt = strcmp(stx, "true");
			if (mt == 0) {
				omcw[1] = (omcw[1] | 2);
			} else {
				omcw[1] = (omcw[1] & ~2);
			}
			solid_dirty = true;
		}
		if (jsoneq(buffer, &t[i], "regionSeparator") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int mt = strcmp(stx, "true");
			if (mt == 0) {
				omcw[1] = (omcw[1] | 4);
			} else {
				omcw[1] = (omcw[1] & ~4);
			}
			solid_dirty = true;
		}
		if (jsoneq(buffer, &t[i], "topPage") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int fp = 0;
			sscanf(stx, "%d", &fp);
			if (fp >= 0 && fp < 255) {
				omcw[2] = fp;
			}
		}
		if (jsoneq(buffer, &t[i], "ldlPage") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int bp = 0;
			sscanf(stx, "%d", &bp);
			if (bp >= 0 && bp < 4) {
				omcw[3] = bp;
			}
		}

	// == pageData JSON

	if (strstr (buffer, sub)) {
		if (jsoneq(buffer, &t[i], "pageNumber") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int pn = 0;
			sscanf(stx, "%d", &pn);
			if (pn >= 0 && pn < 255) {
				scratch[0][0] = pn;
			}
		}
		if (jsoneq(buffer, &t[i], "lineCount") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int lc = 0;
			sscanf(stx, "%d", &lc);
			if (lc >= 0 && lc < 16) {
				scratch[0][1] = lc;
			}
		}
		if (jsoneq(buffer, &t[i], "freeze") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][2] |= 4;
			} else {
				scratch[0][2] &= ~4;
			}
		}
		if (jsoneq(buffer, &t[i], "advisory") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][2] |= 2;
			} else {
				scratch[0][2] &= ~2;
			}
		}
		if (jsoneq(buffer, &t[i], "warning") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][2] |= 1;
			} else {
				scratch[0][2] &= ~1;
			}
		}
		if (jsoneq(buffer, &t[i], "flip") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][3] |= 4;
			} else {
				scratch[0][3] &= ~4;
			}
		}
		if (jsoneq(buffer, &t[i], "roll") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][3] |= 2;
			} else {
				scratch[0][3] &= ~2;
			}
		}
		if (jsoneq(buffer, &t[i], "chain") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][3] |= 1;
			} else {
				scratch[0][3] &= ~1;
			}
		}
		if (jsoneq(buffer, &t[i], "separator") == 0 ||
		    jsoneq(buffer, &t[i], "seperator") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][4 + (2 * (lgp0[0] % 15))] |= 8;
			} else {
				scratch[0][4 + (2 * (lgp0[0] % 15))] &= ~8;
			}
			lgp0[0]++;
		}
		if (jsoneq(buffer, &t[i], "flash") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][4 + (2 * (lgp0[1] % 15))] |= 4;
			} else {
				scratch[0][4 + (2 * (lgp0[1] % 15))] &= ~4;
			}
			lgp0[1]++;
		}
		if (jsoneq(buffer, &t[i], "reverse") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][4 + (2 * (lgp0[2] % 15))] |= 2;
			} else {
				scratch[0][4 + (2 * (lgp0[2] % 15))] &= ~2;
			}
			lgp0[2]++;
		}
		if (jsoneq(buffer, &t[i], "border") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int t = strcmp(stx, "true");
			if (t == 0) {
				scratch[0][4 + (2 * (lgp0[3] % 15))] |= 1;
			} else {
				scratch[0][4 + (2 * (lgp0[3] % 15))] &= ~1;
			}
			lgp0[3]++;
		}
		if (jsoneq(buffer, &t[i], "color") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int cl = 0;
			sscanf(stx, "%d", &cl);
			if (cl >= 0 && cl < 16) {
				scratch[0][5 + (2 * (lgp0[4] % 15))] = cl;
			}
			lgp0[4]++;
		}
		if (jsoneq(buffer, &t[i], "rowNumber") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int rn = 0;
			sscanf(stx, "%d", &rn);
			if (rn >= 0 && rn < 16) {
				scratch[0][34 + (2 * (lgp0[5] % 15))] = rn;
			}
			lgp0[5]++;
		}
		if (jsoneq(buffer, &t[i], "height") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int hx = 0;
			sscanf(stx, "%d", &hx);
			if (hx >= 0 && hx < 4) {
				hx = hx << 2;
				hx = hx & 12;
				scratch[0][35 + (2 * (lgp0[6] % 15))] |= hx;
			}
			lgp0[6]++;
		}
		if (jsoneq(buffer, &t[i], "width") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			int wx = 0;
			sscanf(stx, "%d", &wx);
			if (wx >= 0 && wx < 4) {
				wx = wx & 3;
				scratch[0][35 + (2 * (lgp0[7] % 15))] |= wx;
			}
			lgp0[7]++;
		}
		if (jsoneq(buffer, &t[i], "textData") == 0) {
			sprintf(stx, "%.*s", t[i+1].end - t[i+1].start, buffer + t[i+1].start);
			char data[64] = {0};
			int cadvance = 0;
			strncpy(data, stx, sizeof(data));

			for(int c = 0; c < (int)sizeof(data); c++) {
				if (data[c] == 92 && data[c+1] == 92) {
					cadvance ++;
				}
				scratch[1 + (lgp0[8] % 15)][c] = data[c + cadvance];
			}
			lgp0[8]++;
		}
	}

	}

	if (scratch[0][0] > 0 && scratch[0][0] < 256) {
		for (int l = 0; l < (int)sizeof(scratch[0]); l++) {
			pagestore[16 * scratch[0][0]][l] = scratch[0][l];
		}
		if(scratch[0][1] > 0 && scratch[0][1] < 16) {
			for (int j = 0; j < scratch[0][1]; j++) {
				strncpy(pagestore[(16 * scratch[0][0]) + 1 + j], scratch[1 + j], sizeof(scratch[1 + j]));
			}
			scratch[0][1] = 0;
		}
		scratch[0][0] = 0;
	}

	send(new_socket, hello, strlen(hello), 0);
	sockclose(new_socket);
	sockclose(server_fd);
}

void* tcpServ(void* arg) {
	pthread_detach(pthread_self());
	while(1) {
		createSock();
	}
	pthread_exit(NULL);
}

void tcpEst() {
	pthread_t ptid;
	pthread_create(&ptid, NULL, &tcpServ, NULL);
}

//-------------------------------------------------------------------------
// MWINDOW: Win32 preview window backend
//-------------------------------------------------------------------------

#ifdef MWINDOW

static HWND      win_hwnd = NULL;
static uint32_t *win_present_buf = NULL;   // DISP_W x DISP_H, BGRA for GDI
static BITMAPINFO win_bmi;

static LRESULT CALLBACK winProc(HWND h, UINT msg, WPARAM wp, LPARAM lp)
{
    switch (msg) {
        case WM_KEYDOWN:
            if (wp == VK_ESCAPE || wp == 'Q') run = false;
            return 0;
        case WM_CLOSE:
        case WM_DESTROY:
            run = false;
            PostQuitMessage(0);
            return 0;
        case WM_PAINT: {
            // Repaint from the last composed frame
            PAINTSTRUCT ps;
            HDC dc = BeginPaint(h, &ps);
            if (win_present_buf) {
                StretchDIBits(dc, 0, 0, DISP_W, DISP_H,
                              0, 0, DISP_W, DISP_H,
                              win_present_buf, &win_bmi,
                              DIB_RGB_COLORS, SRCCOPY);
            }
            EndPaint(h, &ps);
            return 0;
        }
    }
    return DefWindowProc(h, msg, wp, lp);
}

static bool winInit(void)
{
    win_present_buf = calloc((size_t)DISP_W * DISP_H, sizeof(uint32_t));
    if (!win_present_buf) return false;

    memset(&win_bmi, 0, sizeof(win_bmi));
    win_bmi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    win_bmi.bmiHeader.biWidth       = DISP_W;
    win_bmi.bmiHeader.biHeight      = -DISP_H;   // negative = top-down
    win_bmi.bmiHeader.biPlanes      = 1;
    win_bmi.bmiHeader.biBitCount    = 32;
    win_bmi.bmiHeader.biCompression = BI_RGB;

    WNDCLASS wc = {0};
    wc.lpfnWndProc   = winProc;
    wc.hInstance     = GetModuleHandle(NULL);
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
    wc.lpszClassName = "scrolltest_preview";
    if (!RegisterClass(&wc)) return false;

    RECT r = { 0, 0, DISP_W, DISP_H };
    AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, FALSE);

    win_hwnd = CreateWindow("scrolltest_preview", "scroll_test preview",
                            WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX,
                            CW_USEDEFAULT, CW_USEDEFAULT,
                            r.right - r.left, r.bottom - r.top,
                            NULL, NULL, wc.hInstance, NULL);
    if (!win_hwnd) return false;

    ShowWindow(win_hwnd, SW_SHOW);
    return true;
}

// Composite background(black) -> solid -> over, convert RGBA -> BGRA,
// and blit to the window.  Called once per frame.
static void winPresent(void)
{
    const uint32_t *sp = (const uint32_t *)solid.image.buffer;
    const uint32_t *op = (const uint32_t *)over.image.buffer;
    uint32_t       *dp = win_present_buf;

    for (int i = 0; i < DISP_W * DISP_H; i++) {
        uint32_t s = sp[i];
        uint32_t o = op[i];

        // base = solid over black
        uint32_t sa = (s >> 24) & 0xFF;
        uint32_t br = (( s        & 0xFF) * sa) / 255;
        uint32_t bg = (((s >>  8) & 0xFF) * sa) / 255;
        uint32_t bb = (((s >> 16) & 0xFF) * sa) / 255;

        // out = over over base
        uint32_t oa = (o >> 24) & 0xFF;
        uint32_t orr, og, ob;
        if (oa == 255) {
            orr =  o        & 0xFF;
            og  = (o >>  8) & 0xFF;
            ob  = (o >> 16) & 0xFF;
        } else if (oa == 0) {
            orr = br; og = bg; ob = bb;
        } else {
            orr = (( o        & 0xFF) * oa + br * (255 - oa)) / 255;
            og  = (((o >>  8) & 0xFF) * oa + bg * (255 - oa)) / 255;
            ob  = (((o >> 16) & 0xFF) * oa + bb * (255 - oa)) / 255;
        }

        // GDI 32-bit DIB byte order is B,G,R,X  ->  uint32 0x00RRGGBB
        dp[i] = (orr << 16) | (og << 8) | ob;
    }

    HDC dc = GetDC(win_hwnd);
    StretchDIBits(dc, 0, 0, DISP_W, DISP_H,
                  0, 0, DISP_W, DISP_H,
                  win_present_buf, &win_bmi, DIB_RGB_COLORS, SRCCOPY);
    ReleaseDC(win_hwnd, dc);
}

static void winPump(void)
{
    MSG msg;
    while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

#endif // MWINDOW

//-------------------------------------------------------------------------

int main(int argc, char *argv[])
{
#ifndef MWINDOW
	struct timespec ntime, ntime1;
	int32_t layer = 8991;
	bcm_host_init();
#else
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
		fprintf(stderr, "WSAStartup failed\n");
		return 1;
	}
#endif

	srand((unsigned)time(NULL));

	//-- Init pagestore
	for (int m = 0; m < 4096; m++) {
		for (int n = 0; n < (int)strlen(pagestore[m]); n++) {
			pagestore[m][n] = 32;
		}
	}
	for (int mm = 1; mm < 256; mm++) {
		for (int nn = 0; nn < 16; nn++) {
			pagestore[16 * mm][5 + (nn * 2)] = 2;
		}
	}

	tcpEst();

	//-- Load TIGA fonts.  01/02/07 are required; others load if present.
	{
		char path[32];
		bool any = false;
		for (int fn = 1; fn <= 7; fn++) {
			snprintf(path, sizeof(path), "FONT%02d.tiga", fn);
			if (loadTigaFont(path, &fonts[fn])) any = true;
			snprintf(path, sizeof(path), "SHDT%02d.tiga", fn);
			loadTigaFont(path, &shadow_fonts[fn]);
		}
		if (!any) {
			fprintf(stderr, "no TIGA fonts could be loaded\n");
			exit(EXIT_FAILURE);
		}
		// Hard requirements for the height mapping
		if (!fonts[1].loaded || !fonts[2].loaded || !fonts[7].loaded) {
			fprintf(stderr,
				"warning: FONT01/FONT02/FONT07 not all present; "
				"missing heights will fall back\n");
		}
	}

	//-- Precompute 768 -> 720 resampling taps
	initHResize();

	//-- Allocate render buffers
	top768    = calloc((size_t)RENDER_W * TOP_H,        sizeof(uint32_t));
	scroll768 = calloc((size_t)RENDER_W * SCROLL_BUF_H, sizeof(uint32_t));
	bot768    = calloc((size_t)RENDER_W * BOT_H,        sizeof(uint32_t));

	top_buf    = calloc((size_t)DISP_W * TOP_H,         sizeof(uint32_t));
	scroll_buf = calloc((size_t)DISP_W * SCROLL_BUF_H,  sizeof(uint32_t));
	bot_buf    = calloc((size_t)DISP_W * BOT_H,         sizeof(uint32_t));

	if (!top768 || !scroll768 || !bot768 ||
	    !top_buf || !scroll_buf || !bot_buf) {
		fprintf(stderr, "failed to allocate render buffers\n");
		exit(EXIT_FAILURE);
	}

#ifndef MWINDOW
	//-- Dispmanx setup
	DISPMANX_DISPLAY_HANDLE_T display = vc_dispmanx_display_open(0);
	assert(display != 0);

	DISPMANX_MODEINFO_T info;
	int result = vc_dispmanx_display_get_info(display, &info);
	assert(result == 0);
	s_width  = info.width;
	s_height = info.height;

	BACKGROUND_LAYER_T backgroundLayer;
	initBackgroundLayer(&backgroundLayer, 0x0000, layer);

	initImageLayer ( &solid, s_width, s_height, VC_IMAGE_RGBA32 );
	createResourceImageLayer( &solid, layer );
	clearImageRGB( &(solid.image), &clear_color);

	initImageLayer ( &over, s_width, s_height, VC_IMAGE_RGBA32 );
	createResourceImageLayer( &over, layer+1 );
	clearImageRGB( &(over.image), &clear_color);
#else
	//-- Preview window setup: solid/over become plain CPU buffers
	s_width  = DISP_W;
	s_height = DISP_H;

	solid.image.width  = DISP_W;
	solid.image.height = DISP_H;
	solid.image.buffer = calloc((size_t)DISP_W * DISP_H, sizeof(uint32_t));

	over.image.width   = DISP_W;
	over.image.height  = DISP_H;
	over.image.buffer  = calloc((size_t)DISP_W * DISP_H, sizeof(uint32_t));

	if (!solid.image.buffer || !over.image.buffer || !winInit()) {
		fprintf(stderr, "preview window init failed\n");
		return 1;
	}
#endif

	imageBoxFilledRGB( &(solid.image), 0, BOT_Y, DISP_W, BOT_Y + 2, &star_white );
	imageBoxFilledRGB( &(solid.image), 0, BOT_Y + 2, DISP_W, DISP_H, &star_purple );
	changeSourceAndUpdateImageLayer( &solid );
	solid_dirty = false;

#ifndef MWINDOW
	DISPMANX_UPDATE_HANDLE_T update = vc_dispmanx_update_start(0);
	assert(update != 0);

	addElementBackgroundLayer( &backgroundLayer, display, update );
	addElementImageLayerOffset( &solid, 0, 0, display, update );
	addElementImageLayerOffset( &over,  0, 0, display, update );

	result = vc_dispmanx_update_submit_sync(update);
	assert(result == 0);
#endif

	//---------------------------------------------------------------------

	int ldli     = 1;
	int ldlcount = 0;

	int  content_h    = TOP_H;
	bool was_rolling  = false;
	int  last_page    = -1;
	char last_botkey[80] = "";   // change-detection for the LDL region

	// Set default omcw state
	omcw[1] = 5;
	omcw[2] = 1;
	omcw[3] = 1;

	while (run) {

		//-- Keyboard
		int kp = 0;
		if (keyPressed(&kp)) {
			kp = tolower(kp);
			switch(kp) {
				case 27: run = false; break;
				case 'q': run = false; break;
				default: break;
			}
		}

		//-- Build clock/date string (logic unchanged from original)
		time_t s = time(NULL);
		struct tm *current_time = localtime(&s);

		int hr  = current_time->tm_hour;
		int min = current_time->tm_min;
		int sec = current_time->tm_sec;

		char chr[3];
		char mer[3];
		sprintf(mer, "AM");
		if (hr == 0)  sprintf(chr, "12");
		if (hr == 12) sprintf(mer, "PM");
		if (hr > 0 && hr > 12) { hr -= 12; sprintf(mer, "PM"); }
		if (hr > 0 && hr < 10) { sprintf(chr, " %01d", hr); }
		else if (hr > 9 && hr < 24) { sprintf(chr, "%02d", hr); }

		static const char *wdays[] =
			{ "Sun","Mon","Tue","Wed","Thu","Fri","Sat" };
		static const char *mons[]  =
			{ "Jan","Feb","Mar","Apr","May","Jun",
			  "Jul","Aug","Sep","Oct","Nov","Dec" };

		char mday[3];
		if (current_time->tm_mday < 10) {
			sprintf(mday, " %01d", current_time->tm_mday);
		} else {
			sprintf(mday, "%02d", current_time->tm_mday);
		}

		char ldlclock[16];
		char ldldate[16];
		sprintf(ldlclock, "%s:%02d:%02d %2s", chr, min, sec, mer);
		sprintf(ldldate,  "%3s %3s %s",
			wdays[current_time->tm_wday % 7],
			mons[current_time->tm_mon % 12], mday);
		char ldlsmallstr0[48] = "";
		sprintf(ldlsmallstr0, "%s           %s", ldldate, ldlclock);

		//-- Solid (background) layer — only update when state changed
		if (solid_dirty) {
			clearImageRGB( &(solid.image), &clear_color );

			if (omcw[1] & 1) {
				imageBoxFilledRGB( &(solid.image), 0, BOT_Y, DISP_W, DISP_H, &star_purple );
			}
			if (omcw[1] & 2) {
				if (pagestore[16 * omcw[2]][5] == 5) {
					imageBoxFilledRGB( &(solid.image), 0, 0, DISP_W, BOT_Y - 1, &star_purple );
				} else if (pagestore[16 * omcw[2]][5] == 0) {
					imageBoxFilledRGB( &(solid.image), 0, 0, DISP_W, BOT_Y - 1, &star_gray );
				} else {
					imageBoxFilledRGB( &(solid.image), 0, 0, DISP_W, BOT_Y - 1, &star_green );
				}
			}
			if (omcw[1] & 4) {
				imageBoxFilledRGB( &(solid.image), 0, BOT_Y, DISP_W, BOT_Y + 1, &star_white );
			}

			changeSourceAndUpdateImageLayer( &solid );
			solid_dirty = false;
		}

		//-- Current top page + roll state
		int  cur_page = (int)(unsigned char)omcw[2];
		bool rolling  = pageValid(cur_page) && pageRoll(cur_page);

		//-- Re-render top content only when page or roll mode changes
		if (cur_page != last_page || rolling != was_rolling) {
			scroll_y = 0;
			if (rolling) {
				buildScrollContent(cur_page, &content_h);
			} else {
				renderTopStatic(cur_page);
				content_h = TOP_H;
			}
			last_page   = cur_page;
			was_rolling = rolling;
		}

		//-- Re-render LDL region only when its visible strings change
		{
			char botkey[80];
			snprintf(botkey, sizeof(botkey), "%d|%d|%s",
			         (int)omcw[3], ldli, ldlsmallstr0);
			if (strcmp(botkey, last_botkey) != 0) {
				renderBot(ldlsmallstr0, ldli);
				strncpy(last_botkey, botkey, sizeof(last_botkey));
				last_botkey[sizeof(last_botkey) - 1] = '\0';
			}
		}

		//-- Blit window + bottom region to the display layer
		blitBuffersToOver(rolling, content_h);

		//-- LDL crawl line advance (every 40 frames, as original)
		ldlcount++;
		if ((ldlcount % 120) == 39) {
			ldli++;
			if (ldli > pagestore[800][1]) {
				ldli = 1;
			}
		}

#ifdef MWINDOW
		//-- Present composed frame in the preview window + pump messages
		winPresent();
		winPump();
		Sleep(16);
#else
		//-- Frame timing (~60fps)
		ntime.tv_sec  = 0;
		ntime.tv_nsec = 16000000;
		nanosleep(&ntime, &ntime1);
#endif
	}

	keyboardReset();

	//-- Cleanup
	for (int fn = 1; fn <= 7; fn++) {
		freeTigaFont(&fonts[fn]);
		freeTigaFont(&shadow_fonts[fn]);
	}
	free(top768);  free(scroll768);  free(bot768);
	free(top_buf); free(scroll_buf); free(bot_buf);

#ifndef MWINDOW
	result = vc_dispmanx_display_close(display);
	assert(result == 0);
#else
	free(solid.image.buffer);
	free(over.image.buffer);
	free(win_present_buf);
	if (win_hwnd) DestroyWindow(win_hwnd);
	WSACleanup();
#endif

	return 0;
}
