/*
 * VARCem	Virtual ARchaeological Computer EMulator.
 *		An emulator of (mostly) x86-based PC systems and devices,
 *		using the ISA,EISA,VLB,MCA  and PCI system buses, roughly
 *		spanning the era between 1981 and 1995.
 *
 *		This file is part of the VARCem Project.
 *
 *		Rendering module for Microsoft Direct2D.
 *
 * Version:	@(#)win_d2d.cpp	1.0.9	2019/05/17
 *
 * Authors:	Fred N. van Kempen, <decwiz@yahoo.com>
 *		David Hrdlicka, <hrdlickadavid@outlook.com>
 *
 *		Copyright 2018,2019 Fred N. van Kempen.
 *		Copyright 2018 David Hrdlicka.
 *
 * 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 the:
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place - Suite 330
 *   Boston, MA 02111-1307
 *   USA.
 */
#define UNICODE
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <d2d1.h>
#include <d2d1helper.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "../emu.h"
#include "../config.h"
#include "../version.h"
#include "../device.h"
#include "../plat.h"
#ifdef USE_LIBPNG
# include "../png.h"
#endif
#ifdef _MSC_VER
# pragma warning(disable: 4200)
#endif
#include "../devices/video/video.h"
#include "win.h"
#include "win_d2d.h"


#define PATH_D2D_DLL	"d2d1.dll"


#if USE_D2D == 2
# define DLLFUNC(x)	D2D1_##x


/* Pointers to the real functions. */
typedef HRESULT (WINAPI *MyCreateFactory_t)(D2D1_FACTORY_TYPE, REFIID, CONST D2D1_FACTORY_OPTIONS*, void**);
static MyCreateFactory_t D2D1_CreateFactory;

static const dllimp_t d2d_imports[] = {
  { "D2D1CreateFactory",	&D2D1_CreateFactory		},
  { NULL,			NULL				}
};

static void			*d2d_handle = NULL;
#else
# define DLLFUNC(x)	D2D1##x
#endif


static HWND			d2d_hwnd, old_hwndMain;
static ID2D1Factory		*d2d_factory;
static ID2D1HwndRenderTarget	*d2d_hwndRT;
static ID2D1BitmapRenderTarget	*d2d_btmpRT;
static ID2D1Bitmap		*d2d_bitmap;
static int			d2d_width, d2d_height,
				d2d_screen_width, d2d_screen_height,
				d2d_fs;
static int			d2d_enabled;


static void
d2d_stretch(float *w, float *h, float *x, float *y)
{
    double dw, dh, dx, dy, temp, temp2, ratio_w, ratio_h, gsr, hsr;

    switch (config.vid_fullscreen_scale) {
	case FULLSCR_SCALE_FULL:
		*w = (float)d2d_screen_width;
		*h = (float)d2d_screen_height;
		*x = 0;
		*y = 0;
		break;

	case FULLSCR_SCALE_43:
	case FULLSCR_SCALE_KEEPRATIO:
		dw = (double)d2d_screen_width;
		dh = (double)d2d_screen_height;

		hsr = dw / dh;
		if (config.vid_fullscreen_scale == FULLSCR_SCALE_43)
			gsr = 4.0 / 3.0;
		else
			gsr = ((double) *w) / ((double) *h);

		if (gsr <= hsr) {
			temp = dh * gsr;
			dx = (dw - temp) / 2.0;
			dw = temp;
			*w = (float) dw;
			*h = (float) dh;
			*x = (float) dx;
			*y = 0;
		} else {
			temp = dw / gsr;
			dy = (dh - temp) / 2.0;
			dh = temp;
			*w = (float) dw;
			*h = (float) dh;
			*x = 0;
			*y = (float) dy;
		}
		break;
 
	case FULLSCR_SCALE_INT:
		dw = (double) d2d_screen_width;
		dh = (double) d2d_screen_height;
		temp = ((double) *w);
		temp2 = ((double) *h);
		ratio_w = dw / ((double) *w);
		ratio_h = dh / ((double) *h);
		if (ratio_h < ratio_w)
			ratio_w = ratio_h;
		dx = (dw / 2.0) - ((temp * ratio_w) / 2.0);
		dy = (dh / 2.0) - ((temp2 * ratio_h) / 2.0);
		dw -= (dx * 2.0);
		dh -= (dy * 2.0);
		*w = (float) dw;
		*h = (float) dh;
		*x = (float) dx;
		*y = (float) dy;
		break;
    }
}


static void
d2d_blit(bitmap_t *scr, int x, int y, int y1, int y2, int w, int h)
{
    ID2D1Bitmap *fs_bitmap = 0;
    ID2D1RenderTarget *RT;
    D2D1_RECT_U rectU;
    HRESULT hr = S_OK;
    void *srcdata;
    int yy;	
    float fs_x = 0, fs_y = 0;
    float fs_w = (float)w;
    float fs_h = (float)h;

    DEBUG("D2D: blit(x=%d, y=%d, y1=%d, y2=%d, w=%d, h=%d)\n", x,y, y1,y2, w,h);

    if (! d2d_enabled) {
	video_blit_done();
	return;
    }

    // TODO: Detect double scanned mode and resize render target
    // appropriately for more clear picture
    if (w != d2d_width || h != d2d_height) {
	if (d2d_fs) {
		if (d2d_btmpRT) {
			d2d_btmpRT->Release();
			d2d_btmpRT = NULL;
		}
		hr = d2d_hwndRT->CreateCompatibleRenderTarget(
				D2D1::SizeF((float)w, (float)h),
				&d2d_btmpRT);
	} else {
		hr = d2d_hwndRT->Resize(D2D1::SizeU(w, h));
	}

	if (SUCCEEDED(hr)) {
		d2d_width = w;
		d2d_height = h;
	}
    }

    if ((y1 == y2) || (scr == NULL)) {
	video_blit_done();
	return;
    }

    // TODO: Copy data directly from screen to d2d_bitmap
    srcdata = mem_alloc(h * w * 4);
    for (yy = y1; yy < y2; yy++) {
	if ((y + yy) >= 0 && (y + yy) < scr->h) {
		if (config.vid_grayscale || config.invert_display)
			video_transform_copy(
				(uint32_t *)&(((uint8_t *)srcdata)[yy * w * 4]),
				&scr->line[y + yy][x], w);
		else
			memcpy(
				(uint32_t *)&(((uint8_t *)srcdata)[yy * w * 4]),
				&scr->line[y + yy][x], w * 4);
	}
    }
    video_blit_done();

    rectU = D2D1::RectU(0, 0, w, h);
    hr = d2d_bitmap->CopyFromMemory(&rectU, srcdata, w * 4);

    /*
     * In fullscreen mode we first draw offscreen to an intermediate
     * BitmapRenderTarget, which then gets rendered to the actual
     * HwndRenderTarget in order to implement different scaling modes.
     *
     * In windowed mode we draw directly to the HwndRenderTarget.
     */
    if (SUCCEEDED(hr)) {
	RT = d2d_fs ? (ID2D1RenderTarget *)d2d_btmpRT
		    : (ID2D1RenderTarget *)d2d_hwndRT;
	RT->BeginDraw();
	RT->DrawBitmap(d2d_bitmap,
		       D2D1::RectF(0, (float)y1, (float)w, (float)y2),
		       1.0f,
		       D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR,
		       D2D1::RectF(0, (float)y1, (float)w, (float)y2));
	hr = RT->EndDraw();
    }

    if (d2d_fs) {
	if (SUCCEEDED(hr))
		hr = d2d_btmpRT->GetBitmap(&fs_bitmap);

	if (SUCCEEDED(hr)) {
		d2d_stretch(&fs_w, &fs_h, &fs_x, &fs_y);

		d2d_hwndRT->BeginDraw();

		d2d_hwndRT->Clear(D2D1::ColorF(D2D1::ColorF::Black));

		d2d_hwndRT->DrawBitmap(fs_bitmap,
				       D2D1::RectF(fs_x, fs_y, fs_x + fs_w, fs_y + fs_h),
				       1.0f,
				       D2D1_BITMAP_INTERPOLATION_MODE_LINEAR,
				       D2D1::RectF(0, 0, (float)w, (float)h));

		hr = d2d_hwndRT->EndDraw();
	}
    }

    if (FAILED(hr))
	ERRLOG("D2D: blit: error 0x%08lx\n", hr);

    /* Clean up. */
    free(srcdata);
}


static void
d2d_close(void)
{
    DEBUG("D2D: close()\n");

    video_blit_set(NULL);

    if (d2d_bitmap) {
	d2d_bitmap->Release();
	d2d_bitmap = NULL;
    }
    if (d2d_btmpRT) {
	d2d_btmpRT->Release();
	d2d_btmpRT = NULL;
    }
	
    if (d2d_hwndRT) {
	d2d_hwndRT->Release();
	d2d_hwndRT = NULL;
    }

    if (d2d_factory) {
	d2d_factory->Release();
	d2d_factory = NULL;
    }

    if (d2d_hwnd) {
	hwndMain = old_hwndMain;
	plat_set_input(hwndMain);
	DestroyWindow(d2d_hwnd);
	d2d_hwnd = NULL;
	old_hwndMain = NULL;
    }

#if USE_D2D == 2
    /* Quit and unload the DLL if possible. */
    if (d2d_handle != NULL) {
	dynld_close(d2d_handle);
	d2d_handle = NULL;
    }
#endif

    d2d_enabled = 0;
}


static int
d2d_init(int fs)
{
    WCHAR title[200];
    D2D1_HWND_RENDER_TARGET_PROPERTIES props;
    D2D1_FACTORY_OPTIONS options;
    HRESULT hr;

    INFO("D2D: init(fs=%d)\n", fs);

#if USE_D2D == 2
    /* Try loading the DLL. */
    if (d2d_handle == NULL)
	d2d_handle = dynld_module(PATH_D2D_DLL, d2d_imports);
    if (d2d_handle == NULL) {
	ERRLOG("D2D: unable to load '%s', D2D not available.\n", PATH_D2D_DLL);
	return(0);
    } else
	INFO("D2D: module '%s' loaded.\n", PATH_D2D_DLL);
#endif

    ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));
    if (FAILED(DLLFUNC(CreateFactory)(D2D1_FACTORY_TYPE_MULTI_THREADED,
				      __uuidof(ID2D1Factory),
				      &options,
			        reinterpret_cast <void **>(&d2d_factory)))) {
	ERRLOG("D2D: unable to load factory, D2D not available.\n");
	d2d_close();
	return(0);
    }

    if (fs) {
	/*
	 * Direct2D seems to lack any proper fullscreen mode,
	 * therefore we just create a full screen window and
	 * pass its handle to a HwndRenderTarget.
 	 */
	d2d_screen_width = GetSystemMetrics(SM_CXSCREEN);
	d2d_screen_height = GetSystemMetrics(SM_CYSCREEN);

	mbstowcs(title, emu_version, sizeof_w(title));
	d2d_hwnd = CreateWindow(FS_CLASS_NAME,
				title,
				WS_POPUP,
				0, 0, d2d_screen_width, d2d_screen_height,
				HWND_DESKTOP,
				NULL,
				hInstance,
				NULL);

	old_hwndMain = hwndMain;
	hwndMain = d2d_hwnd;

	plat_set_input(d2d_hwnd);
	SetFocus(d2d_hwnd);
	SetWindowPos(d2d_hwnd, HWND_TOPMOST,
		     0,0, d2d_screen_width,d2d_screen_height, SWP_SHOWWINDOW);	

	props = D2D1::HwndRenderTargetProperties(d2d_hwnd,
		D2D1::SizeU(d2d_screen_width, d2d_screen_height));
    } else {
	// HwndRenderTarget will get resized appropriately by d2d_blit,
	// so it's fine to let D2D imply size of 0x0 for now
	props = D2D1::HwndRenderTargetProperties(hwndRender);
    }

    hr = d2d_factory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
					     props, &d2d_hwndRT);
    if (FAILED(hr)) {
	ERRLOG("D2D: unable to create target: error 0x%08lx\n", hr);
	d2d_close();
	return(0);
    }

    // Create a bitmap for storing intermediate data
    hr = d2d_hwndRT->CreateBitmap(D2D1::SizeU(2048, 2048),
				  D2D1::BitmapProperties(D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_IGNORE)),	
				  &d2d_bitmap);
    if (FAILED(hr)) {
	ERRLOG("D2D: unable to create bitmap: error 0x%08lx\n", hr);
	d2d_close();
	return(0);
    }	

    d2d_fs = fs;
    d2d_width = 0;
    d2d_height = 0;

    /* Make sure we get a clean exit. */
    atexit(d2d_close);

    /* Register our renderer! */
    video_blit_set(d2d_blit);

    d2d_enabled = 1;

    return(1);
}


static void
d2d_screenshot(const wchar_t *fn)
{
    // Saving a screenshot of a Direct2D render target is harder than
    // one would think. Keeping this stubbed for the moment
    //	-ryu
    INFO("D2D: screenshot(%ls)\n", fn);
}


/* See if this module is available or not. */
static int
d2d_available(void)
{
    void *handle;

    handle = dynld_module(PATH_D2D_DLL, NULL);
    if (handle != NULL) {
        dynld_close(handle);
        return(1);
    }

    return(0);
}


static void
d2d_enable(int yes)
{
    d2d_enabled = yes;
}


const vidapi_t d2d_vidapi = {
    "d2d",
    "Direct 2D",
    1,
    d2d_init, d2d_close, NULL,
    NULL,
    NULL,
    d2d_enable,
    d2d_screenshot,
    d2d_available
};
