/**
 * VBE.C -- interface into VESA BIOS extensions from DJGPP-compiled
 * MS-DOS programs.
 *
 * Copyright (c) 2025 Paper
 *
 * 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.
**/

#include <stdio.h>
#include <stdint.h>
#include <dpmi.h>
#include <go32.h>
#include <sys/farptr.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include <dos.h>

#include "vbe.h"

/* pack all structures */
#pragma pack(push, 1)

struct VbeInfoBlock {
	char VbeSignature[4];
	uint16_t VbeVersion;
	uint16_t OemStringPtr[2];
	uint8_t Capabilities[4];
	uint16_t VideoModePtr[2];
	uint16_t TotalMemory; /* in units of 64kB blocks */
	uint8_t Reserved[492];
};

struct VbeModeInfoBlock {
	uint16_t Attributes;    /* deprecated */
	uint8_t WindowA;        /* deprecated */
	uint8_t WindowB;        /* deprecated */
	uint16_t Granularity;   /* deprecated */
	uint16_t WindowSize;
	uint16_t SegmentA;
	uint16_t SegmentB;
	uint32_t WinFuncPtr;    /* deprecated */
	uint16_t Pitch;         /* bytes per horizontal line */

	/* --- OPTIONAL UNTIL VBE 1.2 */
	uint16_t Width;         /* in pixels */
	uint16_t Height;        /* in pixels */
	uint8_t WChar;          /* unused */
	uint8_t YChar;          /* unused */
	uint8_t Planes;
	uint8_t BitsPerPixel;
	uint8_t Banks;          /* deprecated */
	uint8_t MemoryModel;
	uint8_t BankSize;       /* deprecated, almost always 64kB but maybe 16kB */
	uint8_t ImagePages;
	uint8_t Reserved0[1];

	/* Direct color fields */
	uint8_t RedMask;
	uint8_t RedPosition;
	uint8_t GreenMask;
	uint8_t GreenPosition;
	uint8_t BlueMask;
	uint8_t BluePosition;
	uint8_t ReservedMask;
	uint8_t ReservedPosition;
	uint8_t DirectColorAttributes;

	/* --- OPTIONAL UNTIL VBE 2.0 */
	uint32_t Framebuffer; /* physical address of the linear framebuffer */

	uint8_t Reserved1[6];

	/* --- OPTIONAL UNTIL VBE 3.0 */
	uint16_t LinBytesPerScanLine;  /* bytes per scan line for linear modes */
	uint8_t BnkNumberOfImagePages; /* number of images for banked modes */
	uint8_t LinNumberOfImagePages; /* number of images for linear modes */
	uint8_t LinRedMaskSize;        /* size of direct color red mask (linear modes) */
	uint8_t LinRedFieldPosition;   /* bit position of lsb of red mask (linear modes) */
	uint8_t LinGreenMaskSize;      /* size of direct color green mask (linear modes) */
	uint8_t LinGreenFieldPosition; /* bit position of lsb of green mask (linear modes) */
	uint8_t LinBlueMaskSize;       /* size of direct color blue mask (linear modes) */
	uint8_t LinBlueFieldPosition;  /* bit position of lsb of blue mask (linear modes) */
	uint8_t LinRsvdMaskSize;       /* size of direct color reserved mask (linear modes) */
	uint8_t LinRsvdFieldPosition;  /* bit position of lsb of reserved mask (linear modes) */
	uint32_t MaxPixelClock;        /* maximum pixel clock (in Hz) for graphics mode */

	uint8_t Reserved2[189];
};

#pragma pack(pop)

#define VMI_ATTRIBUTE_HARDWARE     0x01
#define VMI_ATTRIBUTE_RESERVED     0x02 /* only relevant for VBE < 1.2 */
#define VMI_ATTRIBUTE_TTY          0x04
#define VMI_ATTRIBUTE_COLOR        0x08
#define VMI_ATTRIBUTE_GRAPHICS     0x10
#define VMI_ATTRIBUTE_VGACOMPAT    0x20
#define VMI_ATTRIBUTE_VGAWINMEM    0x40
#define VMI_ATTRIBUTE_LINEARBUFFER 0x80 /* linear framebuffer */
#define VMI_ATTRIBUTE_DOUBLESCAN   0x100 /* ? */
#define VMI_ATTRIBUTE_INTERLACE    0x200
#define VMI_ATTRIBUTE_TRIPLEBUF    0x400
#define VMI_ATTRIBUTE_STEREOSCOPIC 0x800 /* huh? 3D?? */

/* check if a selection of attributes is enabled */
#define VMI_ATTRIBUTE(x, a) (((x) & (a)) == (a))

#define MIN(a, b) ((a)<(b)?(a):(b))

#define VBE_FARPTR(x) (((x)[1] * 16) + (x)[0])

/* ------------------------------------------------------------------------ */
/* this code is kind of a mess, sorry... */

/* loosely based off the information on the osdev wiki,
 * thanks a lot!
 *
 * this function is a thin wrapper around real-mode
 * VESA routines. `id` is the identifier that goes into
 * the AX register, and `ptr` and `s` are the pointer and 
 * size the resulting structure is supposed to be.
 * `cx`, is obviously the "cx" register. this only needs
 * to be provided if the underlying function requires it. */
static int vesa_get_block(uint16_t id, uint16_t cx, void *ptr, size_t s)
{
	__dpmi_regs r;
	long dosbuf;
	size_t c;

	/* use the conventional memory transfer buffer */
	dosbuf = (__tb & 0xFFFFF);

	/* zero-initialize */
	for (c = 0; c < s; c++)
		_farpokeb(_dos_ds, dosbuf + c, 0);

	/* this isn't necessary for functions other than 
	 * 0x4F00, but I suppose it won't hurt. */
	dosmemput("VBE2", 4, dosbuf);

	r.x.ax = id;
	r.x.cx = cx;
	r.x.di = dosbuf & 0xF;
	r.x.es = (dosbuf >> 4) & 0xFFFF;
	__dpmi_int(0x10, &r);

	/* high byte is the result code,
	 * low byte must always be 0x4F */
	if (r.x.ax != 0x004F)
		return -1;

	/* copy the resulting data into our structure */
	dosmemget(dosbuf, s, ptr);

	return 0;
}

static int vesa_get_info(struct VbeInfoBlock *block)
{
	if (vesa_get_block(0x4F00, 0, block, sizeof(*block)) != 0)
		return -1;

	if (memcmp(block->VbeSignature, "VESA", 4) != 0)
		return -1;

	return 0;
}

/* This could probably be a public API. I just don't care enough. :) */

typedef void (*vesa_video_mode_cb_spec)(uint16_t mode,
	struct VbeModeInfoBlock *pvmi, void *userdata);

static int vesa_enum_video_modes(const struct VbeInfoBlock *pvbe,
	vesa_video_mode_cb_spec cb, void *userdata)
{
	struct VbeModeInfoBlock vmi;
	uint16_t mode;
	uint32_t ptr;

	for (ptr = VBE_FARPTR(pvbe->VideoModePtr); /* nothing */; ptr += 2) {
		mode = _farpeekw(_dos_ds, ptr);
		if (mode == 0xFFFF)
			break; /* end marker */

		if (vesa_get_block(0x4F01, mode, &vmi, sizeof(vmi)) != 0)
			continue; /* ...? */

		cb(mode, &vmi, userdata);
	}

	return 0;
}

struct vesa_best_mode_info {
	uint16_t width;
	uint16_t height;

	/* video mode score; higher is better */
	int16_t score;
	/* video mode */
	uint16_t mode;
	struct VbeModeInfoBlock vmi;
};

static void vesa_best_mode_cb(uint16_t mode, struct VbeModeInfoBlock *pvmi,
	void *userdata)
{
	struct vesa_best_mode_info *info = userdata;
	int16_t score = 0;

	/* need a graphics mode. linear framebuffer is optional ;)
	 * we also need the "extended" bits that were optional in VBE <1.2.
	 * newer versions require that this bit be 1, so... */
	if (!VMI_ATTRIBUTE(pvmi->Attributes, VMI_ATTRIBUTE_RESERVED|VMI_ATTRIBUTE_COLOR|VMI_ATTRIBUTE_GRAPHICS|VMI_ATTRIBUTE_HARDWARE))
		return;

	/* put significant emphasis on resolution, then 
	 * prefer higher bpp if available. */
	if (!(pvmi->Width % info->width))
		score += 16 / (pvmi->Width / info->width);

	if (!(pvmi->Height % info->height))
		score += 16 / (pvmi->Width / info->width);

	score += (pvmi->BitsPerPixel / 8);

	if (score > info->score) {
		info->score = score;
		info->mode = mode;
		memcpy(&info->vmi, pvmi, sizeof(info->vmi));
	}
}

static int vesa_find_best_mode(struct VbeInfoBlock *vbe, uint16_t width,
	uint16_t height, uint16_t *pmode, struct VbeModeInfoBlock *pvmi)
{
	struct vesa_best_mode_info bmi = {0};

	/* start at non-zero, so we always get at least one valid video mode
	 * (that is, if the video card actually gives us any) */
	bmi.width = width;
	bmi.height = height;
	bmi.score = -1;

	vesa_enum_video_modes(vbe, vesa_best_mode_cb, &bmi);

	if (bmi.score == -1)
		return -1;

	*pmode = bmi.mode;
	memcpy(pvmi, &bmi.vmi, sizeof(*pvmi));

	return 0;
}

static int vesa_set_mode(uint16_t mode)
{
	__dpmi_regs r;

	r.x.ax = 0x4F02;
	r.x.bx = mode;

	__dpmi_int(0x10, &r);

	if (r.x.ax != 0x004F)
		return -1;

	return 0;
}

static int vesa_set_bank(int bank_number)
{
	__dpmi_regs r;

	r.x.ax = 0x4F05;
	r.x.bx = 0;
	r.x.dx = bank_number;

	__dpmi_int(0x10, &r);

	if (r.x.ax != 0x004F)
		return -1;

	return 0;
}

/* ------------------------------------------------------------------------ */
/* plain VGA stuff for resetting the video mode */

static void vga_set_mode(uint8_t mode)
{
	__dpmi_regs r;

	r.h.ah = 0x00;
	r.h.al = mode;

	__dpmi_int(0x10, &r);
}

/* ------------------------------------------------------------------------ */
/* public functions and structures */

static int vesa_init_fb(struct vbe *vbe, uint16_t mode, uint32_t fb_address)
{
	/* since we're running from protected mode, we have to allocate
	 * a logical address that maps to the physical framebuffer. */
	__dpmi_meminfo mi;

	return -1;

	if (vesa_set_mode(mode | 0x4000) != 0)
		return -1;

	mi.address = fb_address;
	mi.size    = vbe->size;

	if (__dpmi_physical_address_mapping(&mi) != 0)
		return -1;

	vbe->blit.fb.selector = __dpmi_allocate_ldt_descriptors(1);
	if (vbe->blit.fb.selector == -1)
		return -1;

	if (__dpmi_set_segment_base_address(vbe->blit.fb.selector, mi.address) != 0)
		return -1;

	if (__dpmi_set_segment_limit(vbe->blit.fb.selector, mi.size - 1) != 0)
		return -1;

	vbe->blit.fb.address = mi.address;

	return 0;
}

int vbe_init(struct vbe *vbe, uint16_t width, uint16_t height)
{
	struct VbeInfoBlock vib;
	struct VbeModeInfoBlock vmi;
	uint16_t mode;

	memset(vbe, 0, sizeof(*vbe));

	if (vesa_get_info(&vib) != 0)
		return -1;

	if (vesa_find_best_mode(&vib, width, height, &mode, &vmi) != 0)
		return -1;

	/* I'm fairly sure this can be 15, for 5R-5G-5B. Whatever. :) */
	if (vmi.BitsPerPixel % 8)
		return -1;

	vbe->width = vmi.Width;
	vbe->height = vmi.Height;
	vbe->pitch = vmi.Pitch;
	vbe->bpp = (vmi.BitsPerPixel / 8);

	vbe->size = (vbe->pitch * vbe->height * vbe->bpp);

	if (VMI_ATTRIBUTE(vmi.Attributes, VMI_ATTRIBUTE_LINEARBUFFER)
		&& !vesa_init_fb(vbe, mode, vmi.Framebuffer)) {
		vbe->blit_type = VBE_BLIT_FRAMEBUFFER;
	} else if (!vesa_set_mode(mode)) {
		vbe->blit_type = VBE_BLIT_BANKS;

		vbe->blit.banks.size = vmi.WindowSize * 1024;
		vbe->blit.banks.granularity = vmi.Granularity * 1024;
	} else {
		return -1;
	}

	return 0;
}

static inline __attribute__((__always_inline__)) void farcpy(int selector,
	uint32_t offset, const void *ptr, uint32_t size)
{
	if (!size)
		return; /* LOL */

	/* do the bulk of the copying with longs */
	while (size >= 4) {
		_farpokel(selector, offset, *(const uint32_t *)ptr);
		ptr = (const char *)ptr + 4;
		offset += 4;
		size -= 4;
	}

	/* now, copy any remaining bytes. */
	while (size > 0) {
		_farpokeb(selector, offset, *(const uint8_t *)ptr);
		ptr = (const char *)ptr + 1;
		offset++;
		size--;
	}
}

void vbe_blit(struct vbe *vbe, const void *ptr)
{
	switch (vbe->blit_type) {
	case VBE_BLIT_FRAMEBUFFER: {
		farcpy(vbe->blit.fb.selector, 0, ptr, vbe->size);
		break;
	}
	case VBE_BLIT_BANKS: {
		const uint32_t bank_size = vbe->blit.banks.size;
		const uint32_t bank_granularity = vbe->blit.banks.granularity;
		int bank_number = 0;
		int todo = vbe->size;

		while (todo > 0) {
			int copy_size;

			/* select the appropriate bank */
			vesa_set_bank(bank_number);

			/* how much can we copy in one go? */
			copy_size = MIN(todo, bank_size);

			/* copy a bank of data to the screen */
			dosmemput(ptr, copy_size, 0xA0000);

			/* move on to the next bank of data */
			todo -= copy_size;
			ptr = (const char *)ptr + copy_size;
			bank_number += (bank_size / bank_granularity);
		}
		break;
	}
	default:
		/* nope ? */
		break;
	}
}

void vbe_quit(struct vbe *vbe)
{
	/* reset VGA mode to DOS text mode */
	vga_set_mode(0x03);
}
