/*
	vid_win.c

	Win32 SW vid component

	Copyright (C) 1996-1997  Id Software, Inc.

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "winquake.h"
#include <ddraw.h>

#include "QF/cvar.h"
#include "QF/qargs.h"
#include "QF/sys.h"
#include "QF/vid.h"

#include "context_win.h"
#include "r_internal.h"
#include "vid_internal.h"
#include "vid_sw.h"

typedef union {
	byte        bgra[4];
	uint32_t    value;
} win_palette_t;

static win_palette_t st2d_8to32table[256];
static byte     current_palette[768];
static int      palette_changed;

static LPDIRECTDRAW dd_Object;
static HINSTANCE   hInstDDraw;
static LPDIRECTDRAWSURFACE dd_frontbuffer;
static LPDIRECTDRAWSURFACE dd_backbuffer;
static RECT        src_rect;
static RECT        dst_rect;
static HDC         win_gdi;
static HDC         dib_section;
static HBITMAP     dib_bitmap;
static HGDIOBJ     previous_dib;
static int         using_ddraw;

static LPDIRECTDRAWCLIPPER dd_Clipper;

typedef     HRESULT (WINAPI *ddCreateProc_t) (GUID FAR *,
													 LPDIRECTDRAW FAR *,
													 IUnknown FAR *);
static ddCreateProc_t ddCreate;

static int         dd_window_width = 640;
static int         dd_window_height = 480;

static sw_framebuffer_t swfb;
static framebuffer_t fb = { .buffer = &swfb };

static void
DD_UpdateRects (int width, int height)
{
	POINT       p = { .x = 0, .y = 0 };
	// first we need to figure out where on the primary surface our window
	// lives
	ClientToScreen (win_mainwindow, &p);
	GetClientRect (win_mainwindow, &dst_rect);
	OffsetRect (&dst_rect, p.x, p.y);
	SetRect (&src_rect, 0, 0, width, height);
}


static void
VID_CreateDDrawDriver (int width, int height)
{
	DDSURFACEDESC ddsd;

	using_ddraw = false;
	dd_window_width = width;
	dd_window_height = height;

	fb.width = width;
	fb.height = height;
	swfb.color = malloc (width * height);
	swfb.rowbytes = width;

	if (!(hInstDDraw = LoadLibrary ("ddraw.dll"))) {
		return;
	}
	if (!(ddCreate = (ddCreateProc_t) GetProcAddress (hInstDDraw,
													  "DirectDrawCreate"))) {
		return;
	}

	if (FAILED (ddCreate (0, &dd_Object, 0))) {
		return;
	}
	if (FAILED (dd_Object->lpVtbl->SetCooperativeLevel (dd_Object,
														win_mainwindow,
														DDSCL_NORMAL))) {
		return;
	}

	// the primary surface in windowed mode is the full screen
	memset (&ddsd, 0, sizeof (ddsd));
	ddsd.dwSize = sizeof (ddsd);
	ddsd.dwFlags = DDSD_CAPS;
	ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_VIDEOMEMORY;

	// ...and create it
	if (FAILED (dd_Object->lpVtbl->CreateSurface (dd_Object, &ddsd,
												  &dd_frontbuffer, 0))) {
		return;
	}

	// not using a clipper will slow things down and switch aero off
	if (FAILED (IDirectDraw_CreateClipper (dd_Object, 0, &dd_Clipper, 0))) {
		return;
	}
	if (FAILED (IDirectDrawClipper_SetHWnd (dd_Clipper, 0, win_mainwindow))) {
		return;
	}
	if (FAILED (IDirectDrawSurface_SetClipper (dd_frontbuffer,
											   dd_Clipper))) {
		return;
	}

	// the secondary surface is an offscreen surface that is the currect
	// dimensions
	// this will be blitted to the correct location on the primary surface
	// (which is the full screen) during our draw op
	memset (&ddsd, 0, sizeof (ddsd));
	ddsd.dwSize = sizeof (ddsd);
	ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
	ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;
	ddsd.dwWidth = width;
	ddsd.dwHeight = height;

	if (FAILED (IDirectDraw_CreateSurface (dd_Object, &ddsd,
										   &dd_backbuffer, 0))) {
		return;
	}

	// direct draw is working now
	using_ddraw = true;

	// create initial rects
	DD_UpdateRects (dd_window_width, dd_window_height);
}

static void
VID_CreateGDIDriver (int width, int height, const byte *palette, byte **buffer,
					 int *rowbytes)
{
	// common bitmap definition
	typedef struct dibinfo {
		BITMAPINFOHEADER header;
		RGBQUAD     acolors[256];
	} dibinfo_t;

	dibinfo_t   dibheader;
	BITMAPINFO *pbmiDIB = (BITMAPINFO *) & dibheader;
	int         i;
	byte       *dib_base = 0;

	win_gdi = GetDC (win_mainwindow);
	memset (&dibheader, 0, sizeof (dibheader));

	// fill in the bitmap info
	pbmiDIB->bmiHeader.biSize = sizeof (BITMAPINFOHEADER);
	pbmiDIB->bmiHeader.biWidth = width;
	pbmiDIB->bmiHeader.biHeight = height;
	pbmiDIB->bmiHeader.biPlanes = 1;
	pbmiDIB->bmiHeader.biCompression = BI_RGB;
	pbmiDIB->bmiHeader.biSizeImage = 0;
	pbmiDIB->bmiHeader.biXPelsPerMeter = 0;
	pbmiDIB->bmiHeader.biYPelsPerMeter = 0;
	pbmiDIB->bmiHeader.biClrUsed = 256;
	pbmiDIB->bmiHeader.biClrImportant = 256;
	pbmiDIB->bmiHeader.biBitCount = 8;

	// fill in the palette
	for (i = 0; i < 256; i++) {
		// d_8to24table isn't filled in yet so this is just for testing
		dibheader.acolors[i].rgbRed = palette[i * 3];
		dibheader.acolors[i].rgbGreen = palette[i * 3 + 1];
		dibheader.acolors[i].rgbBlue = palette[i * 3 + 2];
	}

	// create the DIB section
	dib_bitmap = CreateDIBSection (win_gdi,
								   pbmiDIB,
								   DIB_RGB_COLORS,
								   (void **) &dib_base, 0, 0);

	// set video buffers
	if (pbmiDIB->bmiHeader.biHeight > 0) {
		// bottom up
		buffer[0] = dib_base + (height - 1) * width;
		rowbytes[0] = -width;
	} else {
		// top down
		buffer[0] = dib_base;
		rowbytes[0] = width;
	}

	// clear the buffer
	memset (dib_base, 0xff, width * height);

	if ((dib_section = CreateCompatibleDC (win_gdi)) == 0)
		Sys_Error ("DIB_Init() - CreateCompatibleDC failed\n");

	if ((previous_dib = SelectObject (dib_section, dib_bitmap)) == 0)
		Sys_Error ("DIB_Init() - SelectObject failed\n");

	// create a palette
	VID_InitGamma (palette);
	viddef.vid_internal->set_palette (viddef.vid_internal->data, palette);
}

void
Win_UnloadAllDrivers (void)
{
	// shut down ddraw
	if (swfb.color) {
		free (swfb.color);
		swfb.color = 0;
	}
	if (swfb.depth) {
		free (swfb.depth);
		swfb.depth = 0;
	}

	if (dd_Clipper) {
		IDirectDrawClipper_Release (dd_Clipper);
		dd_Clipper = 0;
	}

	if (dd_frontbuffer) {
		IDirectDrawSurface_Release (dd_frontbuffer);
		dd_frontbuffer = 0;
	}

	if (dd_backbuffer) {
		IDirectDrawSurface_Release (dd_backbuffer);
		dd_backbuffer = 0;
	}

	if (dd_Object) {
		IDirectDraw_Release (dd_Object);
		dd_Object = 0;
	}

	if (hInstDDraw) {
		FreeLibrary (hInstDDraw);
		hInstDDraw = 0;
	}

	ddCreate = 0;

	// shut down gdi
	if (dib_section) {
		SelectObject (dib_section, previous_dib);
		DeleteDC (dib_section);
		dib_section = 0;
	}

	if (dib_bitmap) {
		DeleteObject (dib_bitmap);
		dib_bitmap = 0;
	}

	if (win_gdi) {
		// if win_gdi exists then win_mainwindow must also be valid
		ReleaseDC (win_mainwindow, win_gdi);
		win_gdi = 0;
	}
	// not using ddraw now
	using_ddraw = false;
}

static void
Win_CreateDriver (void)
{
	if (vid_ddraw) {
		VID_CreateDDrawDriver (viddef.width, viddef.height);
	}
	if (!using_ddraw) {
		// directdraw failed or was not requested
		//
		// if directdraw failed, it may be partially initialized, so make sure
		// the slate is clean
		Win_UnloadAllDrivers ();

		VID_CreateGDIDriver (viddef.width, viddef.height, viddef.palette,
							 &swfb.color, &swfb.rowbytes);
	}
}

static void
win_init_bufers (void *data)
{
	sw_ctx_t   *ctx = data;

	ctx->framebuffer = &fb;

	Win_UnloadAllDrivers ();
	Win_CreateDriver ();
}

static void
win_set_palette (sw_ctx_t *ctx, const byte *palette)
{
	palette_changed = 1;
	if (palette != current_palette) {
		memcpy (current_palette, palette, sizeof (current_palette));
	}
	for (int i = 0; i < 256; i++) {
		const byte *pal = palette + 3 * i;
		st2d_8to32table[i].bgra[0] = viddef.gammatable[pal[2]];
		st2d_8to32table[i].bgra[1] = viddef.gammatable[pal[1]];
		st2d_8to32table[i].bgra[2] = viddef.gammatable[pal[0]];
		st2d_8to32table[i].bgra[3] = 255;
	}
	if (!win_minimized && !using_ddraw && dib_section) {
		RGBQUAD     colors[256];
		memcpy (colors, st2d_8to32table, sizeof (colors));
		for (int i = 0; i < 256; i++) {
			colors[i].rgbReserved = 0;
		}
		colors[0].rgbRed = 0;
		colors[0].rgbGreen = 0;
		colors[0].rgbBlue = 0;
		colors[255].rgbRed = 0xff;
		colors[255].rgbGreen = 0xff;
		colors[255].rgbBlue = 0xff;

		if (SetDIBColorTable (dib_section, 0, 256, colors) == 0) {
			Sys_Printf ("win_set_palette() - SetDIBColorTable failed\n");
		}
	}
}

static void
dd_blit_rect (vrect_t *rect)
{
	RECT        TheRect;
	RECT        sRect, dRect;
	DDSURFACEDESC ddsd;

	memset (&ddsd, 0, sizeof (ddsd));
	ddsd.dwSize = sizeof (DDSURFACEDESC);

	// lock the correct subrect
	TheRect.left = rect->x;
	TheRect.right = rect->x + rect->width;
	TheRect.top = rect->y;
	TheRect.bottom = rect->y + rect->height;

	if (IDirectDrawSurface_Lock (dd_backbuffer, &TheRect, &ddsd,
								 DDLOCK_WRITEONLY | DDLOCK_SURFACEMEMORYPTR,
								 0) == DDERR_WASSTILLDRAWING) {
		return;
	}

	// convert pitch to 32-bit addressable
	ddsd.lPitch >>= 2;

	byte       *src = swfb.color + rect->y * swfb.rowbytes + rect->x;
	unsigned   *dst = ddsd.lpSurface;
	for (int y = rect->height; y-- > 0; ) {
		for (int x = rect->width; x-- > 0; ) {
			*dst++ = st2d_8to32table[*src++].value;
		}
		src += swfb.rowbytes - rect->width;
		dst += ddsd.lPitch - rect->width;
	}

	IDirectDrawSurface_Unlock (dd_backbuffer, 0);

	// correctly offset source
	sRect.left = src_rect.left + rect->x;
	sRect.right = src_rect.left + rect->x + rect->width;
	sRect.top = src_rect.top + rect->y;
	sRect.bottom = src_rect.top + rect->y + rect->height;

	// correctly offset dest
	dRect.left = dst_rect.left + rect->x;
	dRect.right = dst_rect.left + rect->x + rect->width;
	dRect.top = dst_rect.top + rect->y;
	dRect.bottom = dst_rect.top + rect->y + rect->height;

	// copy to front buffer
	IDirectDrawSurface_Blt (dd_frontbuffer, &dRect, dd_backbuffer,
							&sRect, 0, 0);
}

static void
win_sw_update (sw_ctx_t *ctx, vrect_t *rects)
{
	vrect_t     full_rect;
	if (!win_palettized && palette_changed) {
		palette_changed = false;
		full_rect.x = 0;
		full_rect.y = 0;
		full_rect.width = viddef.width;
		full_rect.height = viddef.height;
		full_rect.next = 0;
		rects = &full_rect;
	}

	if (using_ddraw) {
		while (rects) {
			dd_blit_rect (rects);
			rects = rects->next;
		}
	} else if (dib_section) {
		while (rects) {
			BitBlt (win_gdi, rects->x, rects->y,
					rects->x + rects->width, rects->y + rects->height,
					dib_section, rects->x, rects->y, SRCCOPY);
			rects = rects->next;
		}
	}
}


static void
win_choose_visual (sw_ctx_t *ctx)
{
}

static void
win_set_background (void)
{
	// because we have set the background brush for the window to 0 (to
	// avoid flickering when re-sizing the window on the desktop), we clear
	// the window to black when created, otherwise it will be empty while
	// Quake starts up.  This also prevents a screen flash to white when
	// switching drivers.  it still flashes, but at least it's black now
	HDC         hdc = GetDC (win_mainwindow);
	PatBlt (hdc, 0, 0, win_rect.right, win_rect.bottom, BLACKNESS);
	ReleaseDC (win_mainwindow, hdc);
}

static void
win_create_context (sw_ctx_t *ctx)
{
	// shutdown any old driver that was active
	Win_UnloadAllDrivers ();

	win_sw_context = ctx;

	win_set_background ();

	// create the new driver
	using_ddraw = false;

	viddef.vid_internal->init_buffers = win_init_bufers;
}

sw_ctx_t *
Win_SW_Context (void)
{
	sw_ctx_t *ctx = calloc (1, sizeof (sw_ctx_t));
	ctx->set_palette = win_set_palette;
	ctx->choose_visual = win_choose_visual;
	ctx->create_context = win_create_context;
	ctx->update = win_sw_update;
	return ctx;
}

void
Win_SW_Init_Cvars (void)
{
}
