// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// 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.

// dvx_video.c  -- Layer 1: Video backend for DVX GUI
//
// Platform-independent video utilities.  The actual VESA/VBE code
// now lives in dvxPlatformDos.c (or the platform file for whatever
// OS we're targeting).
//
// This is the lowest layer of the 5-layer DVX compositor stack
// (video -> draw -> comp -> wm -> app).  It owns the DisplayT context
// that threads through every layer above: screen dimensions, pixel
// format, the backbuffer pointer, and the current clip rectangle.
//
// Design decisions at this layer:
//
// LFB-only (no bank switching):  VBE 2.0 LFB gives the CPU a flat
// address window over the entire framebuffer.  Bank switching (the
// VBE 1.x fallback) requires an INT 10h or port-I/O call every 64 KB,
// which is lethal to throughput in a compositor that redraws hundreds
// of dirty rects per frame.  Requiring LFB eliminates per-scanline
// bank math, allows rep movsd bulk copies, and simplifies every span
// operation in the draw layer.  The tradeoff is dropping cards that
// only expose banked modes  -- acceptable since the target is 486+ with
// a VESA 2.0 BIOS (virtually universal by 1994-95).
//
// System-RAM backbuffer with dirty-rect flushing:  Drawing directly
// to the LFB is slow because VESA framebuffers sit behind the PCI/ISA
// bus and every write-combine flush costs dozens of cycles.  Reads from
// LFB are catastrophically slow (uncached MMIO).  Instead, all draw
// operations target a malloc'd backbuffer in system RAM, which lives
// in the CPU's L1/L2 cache hierarchy.  The compositor (layer 3) then
// copies only the dirty rectangles to the LFB with rep movsd, making
// the bus transfer a single sequential burst per rect.  This also
// means overdraw from occluded windows never touches the bus at all.
//
// Clip rectangle on DisplayT:  Rather than passing a clip rect to
// every draw call (4 extra parameters, extra register pressure on
// i386 calling convention where only a few args go in registers), the
// clip rect is set once on DisplayT before a batch of draw calls.  The
// window manager sets it to the current window's content area before
// calling onPaint, and the compositor sets it to each dirty rect during
// the composite pass.  This keeps the draw API narrow and the common
// case (many draws within the same clip) free of redundant parameter
// shuffling.

#include "dvxVideo.h"
#include "dvxPlat.h"
#include "dvxPal.h"

#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"


// Converts an 8-bit-per-channel RGB triple into whatever pixel
// representation the current display mode uses.  The result is
// always returned as a uint32_t regardless of actual pixel width,
// so callers can store it and pass it through the draw layer without
// caring about the active format.
//
// For 8-bit indexed mode, this falls through to dvxNearestPalEntry
// which searches the 6x6x6 color cube, grey ramp, and chrome slots.
// For direct-color modes (15/16/32-bit), we right-shift each channel
// to discard the low bits that don't fit in the mode's color field,
// then left-shift into position.  This approach truncates rather than
// rounds, which is a deliberate simplicity tradeoff  -- dithering or
// rounding would add per-pixel branches on a 486 for minimal visual
// benefit in a desktop GUI.

uint32_t packColor(const DisplayT *d, uint8_t r, uint8_t g, uint8_t b) {
    if (d->format.bitsPerPixel == 8) {
        return dvxNearestPalEntry(d->palette, r, g, b);
    }

    // Shift each channel: first discard the low bits that won't fit in
    // the mode's field width (e.g. 8-bit red -> 5-bit red for 565),
    // then shift left to the field's position within the pixel word.
    uint32_t rv = ((uint32_t)r >> (8 - d->format.redBits))   << d->format.redShift;
    uint32_t gv = ((uint32_t)g >> (8 - d->format.greenBits)) << d->format.greenShift;
    uint32_t bv = ((uint32_t)b >> (8 - d->format.blueBits))  << d->format.blueShift;

    return rv | gv | bv;
}


// Restores the clip rect to the full screen.  Called after the
// compositor finishes flushing dirty rects, and during init.

void resetClipRect(DisplayT *d) {
    d->clipX = 0;
    d->clipY = 0;
    d->clipW = d->width;
    d->clipH = d->height;
}


// Sets the active clip rectangle, clamping to screen bounds.
// The clip rect is stored directly on DisplayT so every draw call
// in layer 2 can read it without an extra parameter.  This is the
// central mechanism that makes per-window and per-dirty-rect
// clipping cheap: the WM sets clip to the window's content area
// before calling onPaint, and the compositor narrows it further
// to each dirty rect during the composite pass.
//
// Coordinates are converted to min/max form internally, clamped,
// then stored back as x/y/w/h.  If the caller passes a rect that
// lies entirely outside the screen, clipW or clipH will be zero
// and all subsequent draw calls will trivially reject.

void setClipRect(DisplayT *d, int32_t x, int32_t y, int32_t w, int32_t h) {
    int32_t x2 = x + w;
    int32_t y2 = y + h;

    if (x  < 0)          { x  = 0; }
    if (y  < 0)          { y  = 0; }
    if (x2 > d->width)   { x2 = d->width; }
    if (y2 > d->height)  { y2 = d->height; }

    d->clipX = x;
    d->clipY = y;
    d->clipW = (x2 > x) ? x2 - x : 0;
    d->clipH = (y2 > y) ? y2 - y : 0;
}


void unpackColor(const DisplayT *d, uint32_t color, uint8_t *r, uint8_t *g, uint8_t *b) {
    if (d->format.bitsPerPixel == 8) {
        if (color < 256 && d->palette) {
            *r = d->palette[color * 3];
            *g = d->palette[color * 3 + 1];
            *b = d->palette[color * 3 + 2];
        } else {
            *r = *g = *b = 0;
        }
        return;
    }

    uint32_t rv = (color & d->format.redMask)   >> d->format.redShift;
    uint32_t gv = (color & d->format.greenMask) >> d->format.greenShift;
    uint32_t bv = (color & d->format.blueMask)  >> d->format.blueShift;

    *r = (uint8_t)(rv << (8 - d->format.redBits));
    *g = (uint8_t)(gv << (8 - d->format.greenBits));
    *b = (uint8_t)(bv << (8 - d->format.blueBits));
}


// Thin wrapper that delegates to the platform layer.  All the heavy
// lifting  -- VBE enumeration, mode scoring, LFB DPMI mapping,
// backbuffer allocation  -- lives in dvxPlatformDos.c so this file
// stays portable.  The platform layer fills in every field of
// DisplayT (dimensions, pitch, pixel format, lfb pointer,
// backBuf pointer, palette, initial clip rect).

int32_t videoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
    return platformVideoInit(d, requestedW, requestedH, preferredBpp);
}


// Restores text mode, frees the backbuffer and palette, unmaps the
// LFB, and disables DJGPP near pointers.  Must be called before
// exit or the console will be left in graphics mode.

void videoShutdown(DisplayT *d) {
    platformVideoShutdown(d);
}
